콘텐츠로 이동

(KO) CUBRID Log Manager — 코드 수준 심층 분석

이 문서의 위치: 상위 분석서 cubrid-log-manager.md가 설계 의도와 이론적 배경을 다룬다면, 이 문서는 코드 수준에서 모든 분기와 필드를 추적하는 심층 분석서다. 각 챕터는 독립적으로 읽을 수 있지만, 순서대로 읽으면 log record 한 건이 커널 안에서 거치는 전체 생애주기를 따라갈 수 있다.

목차:

Ch제목상태
1자료구조 전체 지도
2초기화와 메모리
3호출자 요청으로부터 Prior Node 만들기
4LSA 할당과 Prior List 부착
5Prior List를 페이지 버퍼로 비우기
6로그 페이지 경계 넘기
7Flush 내구성과 WAL 규칙
8Commit과 Abort 생애주기
9시스템 연산 — Postpone와 Compensation
10Archiving, 헤더 유지보수, 경계 경로

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에 있다. 이 장은 그 규칙이 작동하는 구조체를 기술하며, 규칙 자체를 다루지 않는다.

log sequence address (LSA) 는 논리적 page id와 페이지 내 바이트 offset을 64비트 비트 필드로 압축한다.

// log_lsa -- src/transaction/log_lsa.hpp
struct 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[] 내의 바이트 offsetint64: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 컴파일을 위한 연산자 인라인 래퍼.

on-disk의 모든 record는 고정 크기의 log_rec_header로 시작한다. 이 헤더는 record를 물리적 체인과 트랜잭션별 체인에 연결한다.

// log_rec_header -- src/transaction/log_record.hpp
struct 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동일 트랜잭션의 이전 recordUndo가 한 트랜잭션을 거슬러 올라감
back_lsa물리적 이전 record역방향 log scan
forw_lsa물리적 다음 recordRedo 전방 scan; 후속 record가 확정될 때까지 NULL_LSA (Ch 4)
trid소유 트랜잭션 id뒤섞인 스트림을 역다중화
typeLOG_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가 깨진다.

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.hpp
struct log_data { LOG_RCVINDEX rcvindex; PAGEID pageid; PGLENGTH offset; VOLID volid; };
필드역할존재 이유
rcvindexrecovery dispatch 테이블의 인덱스해당 바이트에 적용할 rv* 함수를 선택
pageid대상 data page idrefix할 페이지
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.hpp
struct 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_undoredoulength+rlength를 가지며(두 블롭의 길이를 구분), log_rec_undolength 하나(undo 이미지만, 논리적 undo), log_rec_redolength 하나(redo 이미지만, 페이지 물리적 redo)를 가진다. MVCC 변형은 이것들을 감싸고 MVCC id와 vacuum 북키핑을 덧붙인다.

// MVCC payload wrappers -- src/transaction/log_record.hpp
struct 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_undoredolog_rec_undoredomvccid, vacuum_infoMVCC 연산을 vacuum이 추적
log_rec_mvcc_undolog_rec_undomvccid, vacuum_infoMVCC delete 계열 연산
log_rec_mvcc_redolog_rec_redomvccid 순수 redo는 vacuum할 버전을 만들지 않음

log_vacuum_info는 undo MVCC record가 가지는 back-pointer다.

// log_vacuum_info -- src/transaction/log_record.hpp
struct log_vacuum_info { LOG_LSA prev_mvcc_op_log_lsa; VFID vfid; };
필드역할존재 이유
prev_mvcc_op_log_lsa이전 MVCC-op record의 LSAvacuum이 log 순서대로 이 체인을 순회
vfid변경이 속한 파일삭제/재사용된 파일을 감지하고 객체 종류를 판단

append 경로는 record를 log_prior_node로 구체화하여 prior list에 연결한다. prior list는 중심적인 staging 구조다(Ch 3–5).

// log_prior_node -- src/transaction/log_append.hpp
struct 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_headerlog_rec_* struct의 길이 + 버퍼가변 데이터와 분리되어 직렬화
ulength/udata, rlength/rdataundo / 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.hpp
struct 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_taildrain 대기 리스트의 head / taildrain 시작점; O(1) append
list_sizestaging된 총 바이트 수flusher가 drain 시점을 결정
prior_flush_list_header분리된 flush 서브리스트의 headdrain이 여기서 가져가므로 producer는 계속 append 가능
prior_lsa_mutex위 모든 필드를 보호하는 mutexLSA 할당 + 연결을 원자적으로 처리

불변식 — 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.hpp
struct 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_lsaAtomic — 아직 flush되지 않은 가장 낮은 LSAWAL watermark; reader/flusher가 prior mutex 없이 경쟁
prev_lsa버퍼에 마지막으로 append된 recordstaging 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_addrlog_crumb

섹션 제목: “1.9 호출자 입력 — log_data_addr와 log_crumb”

호출자 (heap/btree 연산)가 append API에 전달하는 것으로, 위의 모든 구조체는 이로부터 유도된다.

// log_data_addr / log_crumb -- src/transaction/log_append.hpp
struct 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/pageidpgptr에서 추출, vfid에서 오지 않음
log_data_addr.pgptr고정된 data page 포인터volid/pageid를 추출하여 log_data에 기록
log_data_addr.offset변경의 offset/slotlog_data.offset이 됨; 상위 비트는 LOG_RV_RECORD_* 플래그

1.10 on-disk 페이지 구조 — log_hdrpagelog_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.hpp
struct 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 idlog_lsa.pageid와 일치; 읽기 시 동일성 확인
offset여기서 시작하는 첫 번째 record의 offset이전 페이지가 손상된 경우 복구 앵커
flagsTDE 비트 (..._ENCRYPTED_AES/ARIA)LOG_IS_PAGE_TDE_ENCRYPTED가 마스크를 확인
checksum페이지 전체의 CRC32torn page 감지
log_page.hdr위의 header고정 페이지 접두부
log_page.area[]record 바이트LOG_PAGESIZE로 크기 결정

불변식 — LOG_PAGEID -9는 header 페이지다. LOGPB_HEADER_PAGE_ID = -9log_header를 담으며, log record를 전혀 가지지 않고, 모든 archive에 복제된다. 코드는 절대 pageid -9에 일반 record를 기록해서는 안 된다.

1.11 volume header — log_headerlog_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 끝
Recoverychkpt_lsa, smallest_lsa_at_last_chkptrecovery가 시작하는 가장 낮은 LSA
트랜잭션 / MVCCnext_trid, mvcc_next_id, mvcc_op_log_lsa, oldest_visible_mvccid, newest_block_mvccid, vacuum_last_blockid, does_block_need_vacuum다음에 할당할 id; vacuum의 진행 상황
Archivenxarv_pageid, nxarv_phy_pageid, nxarv_num, last_arv_num_for_syscrashes, last_deleted_arv_num, npagesCh 10의 archiving을 구동
Backupbkup_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_delreplication 상태; 정상 종료 플래그
정렬 / 기타dummy, dummy3, dummy4, vol_creation, avg_ntrans, avg_nlocks, was_copied, prefix_name, perm_status_obsoletedummy*는 패딩; vol_creation 시각; avg_* 크기 힌트; was_copied는 복사된 DB를 초기화; prefix_name은 log 접두사; perm_status_obsolete는 레거시

log_arv_header는 각 archive 파일에 찍히는 더 작은 header다.

// log_arv_header -- src/transaction/log_storage.hpp
struct 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_tridarchive 시점의 다음 tridrecovery 컨텍스트
npages이 archive의 페이지 수페이지 범위 경계
fpageid물리적 슬롯 1의 논리적 pageid물리 → 논리 페이지 변환
arv_numarchive 시퀀스 번호log_header.nxarv_num 체인과 일치
dummy, dummy2정렬 패딩on-disk 레이아웃 안정화
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의 구조체들이 어떻게 연결되는지 보여주는 다이어그램.

수정자가 일관성을 유지해야 하는 LSA/포인터 엣지:

  • Physical chainlog_rec_header.forw_lsa/back_lsa; prev_tranlsa.
  • Staging 할당자log_prior_lsa_info.prior_lsa/prev_lsa.
  • Durability watermarklog_append_info.nxio_lsa.
  • Vacuum chainlog_vacuum_info.prev_mvcc_op_log_lsa.
  1. 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)는 변하지 않는 상수다.
  2. log_lsa는 48:16 비트 필드 clock이며, 그 전순서가 모든 durability 결정의 기반이다. log_rec_header는 각 record를 물리적 이중 연결 체인트랜잭션별 체인에 연결하고, type은 append-only·구멍 보존 방식의 log_rectype을 판별한다.
  3. MVCC 래퍼는 mvccid와 (undo에만) log_vacuum_info를 추가한다. redo 래퍼는 vacuum_info를 생략한다 — 순수 redo는 vacuum할 버전을 만들지 않기 때문이다.
  4. prior_lsa_mutexLSA 할당과 연결을 원자적으로 만들고, nxio_lsaatomic WAL watermark다. 이 두 동시성 불변식이 append 경로를 지탱한다. on-disk 페이지는 pageid -9log_header용으로 예약하며, log_headernxarv_*log_arv_header를 구동한다.

독자가 가져야 할 질문은 이것이다. 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에 넘긴다.

모든 것은 프로세스 전역 싱글턴 하나인 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_infonew한다. 전체 필드:

필드역할존재 이유 / 생성자 초기값
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.
hdron-disk LOG_HEADER의 RAM 내 사본 (append_lsa/eof_lsa가 여기에 위치)페이지 -9를 반복 읽는 것을 피함.
archive현재 archive 디스크립터 캐시원하는 페이지가 archive로 넘어갔을 때 사용.
run_nxchkpt_atpageid다음 checkpoint가 발동될 page idcreate/init 중에는 NULL_PAGEID; init 끝에서 재계산.
flushed_lsa_lower_bound / chkpt_lsa_lockSERVER_MODE 전용: flush 조정 LSA + checkpoint-LSA mutexNULL_LSA / PTHREAD_MUTEX_INITIALIZER.
chkpt_redo_lsa / chkpt_every_npagesredo 시작 LSA + checkpoint 빈도NULL_LSA / INT_MAX (후자는 PRM_ID_LOG_CHECKPOINT_NPAGES에서).
rcv_phase / rcv_phase_lsaRecovery 단계 + 해당 LSALOG_RECOVERY_ANALYSIS_PHASE / NULL_LSA; log_final이 단계를 초기화.
backup_in_progress / final_restored_lsa#if 쌍: SERVER backup 플래그 vs SA 마지막 복원 LSA빌드마다 하나; false / NULL_LSA.
loghdr_pgptrheader I/O용 LOG_PAGESIZE 스크래치 페이지log_initialize_internal에서 malloc하는 전역 버퍼, log_final에서 해제 — create 경로의 동명 지역 변수(§2.2)와 구분됨.
flush_infotoflush[] + 카운터 + mutexflush 시 디스크에 밀어낼 dirty append 페이지; §2.4.
group_commit_infogroup commit용 mutex+condcommitter들이 fsync를 묶을 수 있게 함.
writer_infoHA log-writer 상태생성자에서만 new; ~log_global에서 delete.
bg_archive_info백그라운드 archiving 디스크립터PRM_ID_LOG_BACKGROUND_ARCHIVING이 켜져 있으면 init 마지막에 초기화.
mvcc_table / unique_stats_tableMVCC 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 아래에서 실행된다. 모든 분기:

  1. 잔류 상태 확인: trantable.area != NULLlog_final (§2.7).
  2. umask; logpb_initialize_pool (§2.3)이 ring을 할당. 오류 → goto error.
  3. logpb_initialize_log_nameslog_Name_active 등을 구성. 오류 → goto error.
  4. logpb_initialize_header (&log_Gl.hdr, ...)가 RAM 내 header를 채움 (페이지 수, db_logpagesize = LOG_PAGESIZE). 오류 → goto error.
  5. logpb_create_header_page가 page--9 버퍼를 스택 지역 loghdr_pgptr에 할당 — 이것은 log_create_internal 내부에 선언된 변수로, §2.1의 전역 log_Gl.loghdr_pgptr이 아니다. create 경로는 재시작 경로의 I/O 버퍼와 분리된 스크래치 페이지를 사용한다.
  6. 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 */
  1. 빈 append 페이지를 dirty로 표시; logpb_flush_pages_direct가 end-of-log 마크를 기록.
  2. RAM 내 hdr을 지역 loghdr_pgptr->areamemcpy; logpb_flush_page가 페이지 -9를 기록 (오류 → goto error; CUBRID_DEBUG 아래에서는 읽어서 assert 확인).
  3. log_pgptr 초기화, 언마운트, volume-info/log-info 파일 생성, logpb_add_volume으로 active + backup-info volume 등록.
  4. 정상 종료: 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로 디스패치한다. 순서에 따른 모든 분기:

  1. 클린 상태 확인: trantable.area != NULL → log_final.
  2. log-names 초기화: logpb_initialize_log_names 실패는 단순 전파가 아닌 치명적 오류 (logpb_fatal_errorgoto error).
  3. loghdr_pgptr malloc: 전역 log_Gl.loghdr_pgptr (페이지--9 I/O 버퍼, logpb_fetch_header/logpb_flush_header용); NULL → 치명적 + goto error. log_final(§2.7)과 error: 레이블에서 해제.
  4. Pool 초기화: logpb_initialize_pool (§2.3); 오류 → goto error.
  5. fileio_mountNULL_VOLDES 반환 시 두 경로로 나뉜다 — 미디어 충돌 (ismedia_crash != false): 근사 header를 합성한다 (logpb_initialize_header로 기하 구조 설정 후, 아래 강제 필드들이 모든 것을 미체크포인트 상태로 표시, LOG_RESET_APPEND_LSAprior_info에 동기화, chkpt_lsa null 처리, nxarv_* 최대화); 아닐 경우 error_code = ER_IO_MOUNT_FAIL; goto error:
    // log_initialize_internal -- src/transaction/log_manager.c
    log_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);
  6. vdes가 NULL이 아닌 경우: logpb_fetch_header (&log_Gl.hdr)가 실제 페이지 -9를 미러에 읽어들임.
  7. hdr.chkpt_lsachkpt_redo_lsa로 복사. restore_slave 분기 (ismedia_crash && r_args && r_args->restore_slave): HA slave restore를 위해 db_creation, smallest_lsa_at_last_chkpt, append_lsar_args로 복사.
  8. 접두사 이름 불일치: strcmp(hdr.prefix_name, prefix_logname) != 0ER_LOG_INCOMPATIBLE_PREFIX_NAME (알림) 후 계속 진행.
  9. 페이지 크기 불일치 → 재귀 재초기화: hdr.db_iopagesize != IO_PAGESIZE || hdr.db_logpagesize != LOG_PAGESIZEdb_set_page_size, logpb_finalize_pool, 언마운트, LOG_CS_EXIT, logtb_define_trantable_log_latch 재실행 후 log_initialize_internal 재귀 호출 후 반환 — 버퍼가 올바른 크기로 재건됨 (§2.8 참조).
  10. 호환성 검사 (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).
  11. 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.
  12. Prior/append LSA assert + reset (§2.5 참조): rcv_phase = LOG_RESTARTED 설정 후, append.prev_lsa/hdr.append_lsaprior_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.c
struct 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 페이지의 논리적 idNULL_PAGEID = 비어있음; 조회가 이 값을 키로 사용. volatile — lock 없이 읽힘.
phy_pageidactive-log 파일의 물리적 offset변환 캐시로, 각 flush마다 logpb_to_physical_pageid를 건너뜀.
dirty페이지가 디스크와 다름슬롯을 toflush[]에 추가할지 결정.
logpage공유 pages_area 슬랩 내의 포인터작은 디스크립터를 LOG_PAGESIZE payload에서 분리.

분기 완전 기술 (LOG_CS_OWN_WRITE_MODE assert):

  1. log_append_init_zip (§2.6) — 압축 컨텍스트가 ring보다 먼저 올라옴.
  2. logpb_Initialized이면 logpb_finalize_pool (재진입 안전), 그 후 assert pages_area == NULL.
  3. num_buffers = prm_get_integer_value (PRM_ID_LOG_NBUFFERS).
  4. malloc buffers. NULLer_set + ER_OUT_OF_VIRTUAL_MEMORY 반환 (해제할 pool 없음).
  5. malloc pages_area (num_buffers * LOG_PAGESIZE). NULLfree_and_init(buffers), 반환.
  6. 슬랩을 LOG_PAGE_INIT_VALUEmemset; 루프에서 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)으로 설정.
  7. malloc header_page (LOG_PAGESIZE 하나); NULL → 이전 두 할당 모두 해제 후 반환. 페이지 -9용 상주 슬롯인 header_buffer에 연결 (LOGPB_HEADER_PAGE_ID == -9).
  8. logpb_initialize_flush_info (§2.4). 오류 → goto error.
  9. partial_append.status = LOGPB_APPENDREC_SUCCESS; 정렬된 스크래치 페이지 포인터 설정.
  10. 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_poollogpb_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.h
struct 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으로 재설정.
toflushpage 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 == NULLassert; max_toflush = num_buffers - 1, num_toflush = 0 설정; toflushnum_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.hpp
struct 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될 노드가 받을 LSAmutex 아래에서 전진시키면 디스크를 건드리지 않고 LSA를 단조 순서로 발급.
prev_lsa이전에 append된 노드의 LSA새 노드가 역방향 체인/undo를 위해 back_lsa를 저장할 수 있게 함.
prior_list_header / prior_list_tailFIFO head (drain이 소비) / tail (O(1) append)drain (Ch 5)이 head를 읽음; 새 노드는 tail에 연결.
list_size큐의 바이트 수drainer/flusher가 밀어낼 시점을 결정.
prior_flush_list_headerflush로 이미 승격된 서브리스트”append됨”과 “flush 중”을 분리.
prior_lsa_mutex서브시스템 전체의 핫 lock모든 LSA 할당이 여기서 직렬화.

생성자는 모든 것을 비어있는 상태로 시드한다. 실제 LSA 시드는 log_initialize_internal로 미뤄지며, 그곳에서 복원된 header LSA를 appendprior_info 양쪽에 복사한다.

// log_prior_lsa_info ctor / LOG_RESET_*_LSA -- src/transaction/log_append.cpp
log_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_sizelog_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에 따라 분기한다:

  1. 압축 비활성화 → log_Zip_support = false, 반환.
  2. SERVER_MODE: log_Zip_support = true; 버퍼는 스레드별로, 첫 사용 시 지연 할당 — log_append_get_zip_undo/_redoif (thread_p->log_zip_undo == NULL) thread_p->log_zip_undo = log_zip_alloc (IO_PAGESIZE);를 수행.
  3. SA-mode: 프로세스 전역 스태틱 log_zip_undo/log_zip_redo 두 개와 IO_PAGESIZE * 2log_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_finallogpb_finalize_pool

섹션 제목: “2.7 Teardown: log_final과 logpb_finalize_pool”

log_final은 정상 종료이자 create/init이 앞서 호출하는 재진입 방지 함수다. 분기 완전 기술:

  1. 서버 daemon과 시스템 트랜잭션 종료; LOG_CS_ENTER; rcv_phase 초기화.
  2. trantable.area == NULL → 초기화되지 않음; 종료.
  3. 아닐 경우 !logpb_is_pool_initialized() → trantable만 있음; logtb_undefine_trantable, 종료.
  4. 아닐 경우 append.vdes == NULL_VOLDES → pool은 있지만 volume 없음; logpb_finalize_pool + logtb_undefine_trantable, 종료.
  5. 아닐 경우 모든 활성 트랜잭션을 abort (log_abort), anyloose_ends 추적; 디스크에 flush (logpb_flush_pages_direct + pgbuf_flush_all + fileio_synchronize_all).
  6. Header 분기: !anyloose_ends && error_code == NO_ERROR이면 hdr.is_shutdown = true 설정 후 chkpt_lsa = append_lsa 스냅 (클린 — 재시작이 recovery를 건너뜀). 아닐 경우 logpb_checkpoint.
  7. 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_pagefree_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을 건드리는)가 버퍼보다 오래 살아남지 않는다.

LOG_PAGELOG_PAGESIZE 바이트다 (storage_common.hdb_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.offsetLOGAREA_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_internaldb_iopagesize != IO_PAGESIZE || db_logpagesize != LOG_PAGESIZE를 검사한다. 불일치 시 db_set_page_size, pool 종료, 언마운트 후 재귀적으로 자신을 다시 호출하여 올바른 크기로 버퍼를 재할당한다. 그렇지 않으면 LOGAREA_SIZE가 잘못된 페이지 크기를 기반으로 계산되어 record가 물리적 페이지 경계를 넘어 걸치게 된다.

  1. 두 진입점, 서로 다른 생존 기간. log_create_internal은 포맷하고, 페이지 -9를 기록하고, pool을 종료한다 (라이브 상태 없음); log_initialize_internal은 마운트하고, 페이지 -9를 읽고, pool을 활성 상태로 유지하며, recovery를 실행한다.
  2. 재시작은 더 풍부한 분기 트리를 가진다 (§2.2b): 치명적 log-names 경로, 전역 loghdr_pgptr malloc, fileio_mount NULL_VOLDES 분기 (미디어 충돌 header 합성 LOGPAGEID_MAX vs ER_IO_MOUNT_FAIL), logpb_fetch_header, restore_slave 복사, 허용되는 접두사 불일치, 재귀 페이지 크기 재초기화, recovery/클린 디스패치.
  3. 두 개의 전역 변수. log_Gl (append/prior/header/flush) vs 별도의 ring log_Pb; flush_info.toflush[]append.log_pgptrlog_Pb 내부를 가리킨다.
  4. ring은 두 개의 병렬 할당이다LOG_BUFFER[] + pages_area 슬랩 하나; 디스크립터 i ↔ 슬랩 슬롯 i, 포인터 산술로 복원. Flush 용량은 num_buffers - 1.
  5. prior list는 비어있는 상태로 시작하며, LSA는 header에서 시드된다LOG_RESET_APPEND_LSA/LOG_RESET_PREV_LSAappendprior_info 양쪽에 적용; init이 양쪽의 일치를 assert한다.
  6. 압축은 모드에 따라 다르다: SA-mode는 프로세스 전역 LOG_ZIP 스태틱, server-mode는 스레드별 지연 할당; log_Zip_support가 단일 게이트이며 부분 실패 시 false.
  7. Teardown은 bring-up의 역순이다. flush-info와 zip을 마지막에 해제; log_finalis_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_recordprior_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.c
LOG_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_crumbslog_can_skip_redo_logging을 사용한다.

// struct log_prior_node -- src/transaction/log_append.hpp
struct 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_encryptedlog 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.cpp
node->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_datanon-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_ABORTLOG_REC_DONETIME, LOG_SYSOP_ENDLOG_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_undohas_redo 모두 설정하고 두 scratch(또는 길이가 0인 side)가 필요하다. LOG_IS_REDO_RECORD_TYPE이면 has_redozip_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.cpp
case 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_DATAaddr에서 채워진다: 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.h
bool 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_sizelog_zip_alloc이 설정한 용량(IO_PAGESIZE + LZ4 bound)이어서 record마다 재할당하지 않아도 된다. Scratch는 log_append_get_zip_undo/_redo를 통해 가져온다: SERVER_MODE에서는 per-threadthread_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.cpp
if (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_*_zipMAKE_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에는 저장된 길이가 담긴다.

MVCC 타입의 경우, 포인터 switch에서 mvccid_p/vacuum_info_p가 NULL이 아닌 채로 남겨지므로 두 가지 추가 작업이 실행된다. MVCCID는 현재 TDES에서 가져오며, 가장 안쪽 sub-transaction id를 우선한다.

// prior_lsa_gen_undoredo_record_from_crumbs -- src/transaction/log_append.cpp
if (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.hpp
struct 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/offsetvolid)는 recovery가 dispatch하고 바이트를 찾는 기준이다. ulength/rlength는 저장된 길이(상위 비트 = 압축됨)다. MVCC 변형은 undoredo를 임베드하여 non-MVCC reader가 코드를 공유하게 하고, 추가로 mvccid(writer의 id, vacuum과 가시성 판단용)와 vacuum_info(prev_mvcc_op_log_lsa 역방향 링크 + 소유 vfid; 역방향 링크는 Chapter 4에서 채워진다)를 더한다.

  1. 공개 log_append_* API는 얇다 — 버퍼를 LOG_CRUMB으로 감싸고 log_append_*_crumbs에 위임하며, 해당 함수가 rcvindex에서 LOG_RECTYPE을 결정하고 다섯 단계의 guard chain을 먼저 실행한다.
  2. 구성은 lock-free이고 자체 소유적이다 — 모든 작업이 prior_lsa_mutex 바깥에서 이루어지며, node는 세 개의 malloc(node, data_header, 페이로드 복사본)을 소유하므로 비동기 drain이 절대로 호출자 메모리를 건드리지 않는다.
  3. 두 allocator가 타입 공간을 분할한다..._crumbsprior_lsa_gen_undoredo_record_from_crumbs(undo/redo 데이터용); ..._dataprior_lsa_gen_record(제어 record용). 후자의 세 분기는 헤더 크기를 결정하고(dummy/decision 타입은 0), ER_OUT_OF_VIRTUAL_MEMORY로 bail하며, 선택적 undo blob을 복사한다.
  4. 핵심 builder는 측정 → 압축 → 타입별 헤더 크기+채우기 → 페이로드 복사 순서로 실행된다. [[fallthrough]] switch가 UNDO/REDO/UNDOREDO 형태에 걸쳐 non-MVCC 레이아웃을 공유한다.
  5. 압축은 페이지가 아닌 node 단위다log_Zip_support와 255바이트 임계값으로 제어되며 per-thread LOG_ZIP scratch를 사용한다(NULL thread_p이면 압축 없음). 양쪽이 모두 크면 log_diff가 발동하고 타입이 *_DIFF_*로 바뀔 수 있다. 압축/raw 선택은 MAKE_ZIP_LEN을 통해 길이의 최상위 비트에만 기록된다.
  6. MVCC record는 TDES에서 MVCCID + vacuum info를 가져온다(sub-id 우선). prev_mvcc_op_log_lsastart_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도, 할당도 없다.

세 구조체가 만난다: node(Chapter 1; 이 챕터에서 쓰는 필드만), 전역 커서, 그리고 내장된 on-disk record 헤더.

log_prior_node (이 챕터에서 쓰는 필드)

섹션 제목: “log_prior_node (이 챕터에서 쓰는 필드)”
필드역할존재 이유
log_headerLOG_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_lengthdata_header의 바이트 길이data-header 영역의 offset 전진량을 결정한다
data_header타입별 record 헤더(예: LOG_REC_SYSOP_END)매칭된 type arm에서 캐스트하여 MVCC/sysop 하위 필드를 읽는다
ulength / udataundo 페이로드 길이 / 버퍼ulength>0이면 undo 데이터 영역의 offset 전진이 발동된다
rlength / rdataredo 페이로드 길이 / 버퍼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다음 물리적 recordanalysis/redo가 순방향으로 순회할 수 있게 한다. 이 record의 크기가 확정된 후에야 알 수 있으므로 prior_lsa_end_append에서 채워진다
trid이 record를 소유하는 transaction idrecovery가 transaction별로 record를 그룹화한다. tdes->trid에서 설정됨
typeLOG_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_lsaprior stream에 마지막으로 추가된 record의 LSA새 node의 back_lsa가 되고, 이후 새 node를 가리키도록 업데이트된다
prior_list_header단방향 prior list의 headdrain 측(Chapter 5)이 head에서 소비한다
prior_list_tailprior list의 tail새 node가 O(1)로 여기 연결된다
list_size아직 flush되지 않고 단계적으로 쌓인 바이트 수logpb_get_memsize()와 비교하여 강제 flush 여부를 결정한다
prior_flush_list_headerdrain 중인 분리된 리스트의 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.cpp
prior_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.hpp
enum 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_lsamvcc_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조건동작
1LOG_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)
2LOG_SYSOP_START_POSTPONEassert (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 아래서)
3LOG_SYSOP_ENDatomic_sysop_start_lsa가 non-null이고 lastparent_lsa < 이면 null 처리; sysop_start_postpone_lsa에도 동일한 검사/null 처리
4LOG_COMMIT_WITH_POSTPONE 또는 LOG_COMMIT_WITH_POSTPONE_OBSOLETErcv.tran_start_postpone_lsa = start_lsa
5LOG_SYSOP_ATOMIC_STARTassert (LSA_ISNULL (rcv.atomic_sysop_start_lsa))rcv.atomic_sysop_start_lsa = start_lsa
6LOG_COMMIT 또는 LOG_ABORTassert (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 출처
atype == LOG_MVCC_UNDO_DATA(LOG_REC_MVCC_UNDO *) node->data_header&mvcc_undo->vacuum_info, mvcc_undo->mvccid
btype == 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_lsaadd_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.cpp
static void
prior_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.cpp
static 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.)

  1. 하나의 mutex가 순서를 정의한다. prior_lsa_mutex는 record 하나당 한 번(또는 _with_lock으로 여러 번에 걸쳐 한 번) 획득된다. 획득 순서가 곧 LSA 순서다(불변식 4-A). 하나의 lock 아래에서 읽고 전진하므로 공유 또는 순서가 뒤집힌 LSA는 없으며 별도 카운터도 필요 없다.
  2. prior_lsa_start_append가 탄생의 순간이다. prior_lsa를 전진 전에 start_lsa로 복사하고(불변식 4-C), trid를 설정하고, prev_tranlsa/back_lsa를 구성하고, forw_lsa를 null로 만든다.
  3. transaction 체인은 worker 여부에 따라 분기한다. Sysop 아래에 있지 않은 worker record는 null prev_tranlsa/head_lsa/tail_lsa를 갖는다(불변식 4-B). 나머지는 체인에 연결하고 tail_lsa/head_lsa/undo_nxlsa를 업데이트한다.
  4. 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에 저장한다.
  5. forw_lsa는 마지막에 확정된다. prior_lsa_end_append가 record를 지나 정렬하고 다음 헤더의 fit을 보장하므로, forw_lsa는 다음 record의 start_lsa와 일치한다.
  6. Offset 헬퍼들이 record 점유 공간을 소비한다. advance_when_doesnot_fit은 fit을 사전 검사하고(유일한 분기), add_align은 소비 후 정렬하며, alignDOUBLE_ALIGNMENT로 반올림하고 page를 넘긴다.
  7. 비용이 큰 작업은 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)에서 다룬다.

잠금 두 개, 역할 두 가지. 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_CS write 모드를 보유한 상태에서 실행된다. drain하는 스레드가 둘이면 append_lsa.offsetlog_pgptr가 원자적으로 갱신되지 못해 레코드가 서로 뒤섞인다. LOG_CS_OWN_WRITE_MODE assert는 이 규칙 위반을 즉시 치명적 오류로 만든다. 아래의 모든 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 핸드오프.

log_prior_node — drain의 기본 단위 (log_append.hpp)

섹션 제목: “log_prior_node — drain의 기본 단위 (log_append.hpp)”
필드역할존재 이유
log_headerlogpb_start_append가 그대로 복사하는 LOG_RECORD_HEADER디스크 상의 레코드 헤더
start_lsaappend 시점에 append_lsa와 반드시 일치해야 함LSN 순서 오염 감지
tde_encrypted목적 페이지가 TDE 암호화 대상임을 표시appending_page_tde_encrypted 설정에 사용
data_header_lengthdata_header의 바이트 길이헤더 복사 크기 결정
data_header레코드 타입별 고정 헤더 페이로드LOG_RECORD_HEADER 뒤에 오는 부분
ulength / udataundo 세그먼트의 길이/포인터rollback 이미지
rlength / rdataredo 세그먼트의 길이/포인터recovery 이미지
next다음 노드 링크LSN 순서 순회에 사용

불변식 — 노드 순서 = LSN 순서. prior_lsa_mutex 아래에서 꼬리에 이어 붙이기 때문에 next 순회는 정확히 LSN 오름차순이 된다. logpb_append_next_recordLSA_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_pageidlog 내에서 이 프레임의 식별자페이지를 물리 슬롯에 매핑
hdr.offset이 페이지에서 첫 레코드가 시작하는 offsetlogpb_start_append가 한 번 설정; 복구 시 salvage 가능
hdr.flagsTDE 암호화 플래그logpb_set_tde_algorithm이 기록
hdr.checksum페이지의 CRC32flush 시 계산됨 (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)래핑된 프레임의 논리 페이지 idflush 대상 검증
phy_pageid (volatile)active log 내의 물리 페이지 id논리 페이지를 디스크 슬롯에 매핑
dirty (bool)“아직 flush되지 않은 변경이 있음”logpb_set_dirty가 올리고, flusher가 내림 (Chapter 7)
logpage (LOG_PAGE*)버퍼링된 페이로드에 대한 역방향 포인터logpb_get_log_bufferLOG_PAGE*로부터 래퍼를 복원

log_append_info — 단일 writer의 커서 상태 (log_append.hpp)

섹션 제목: “log_append_info — 단일 writer의 커서 상태 (log_append.hpp)”
필드역할존재 이유
vdesactive log 볼륨 디스크립터flush 대상; drain 중 변경 없음
nxio_lsa (atomic)아직 디스크에 없는 최소 LSNWAL 프런티어 (Chapter 7)
prev_lsa마지막으로 완전히 append된 레코드의 주소logpb_start_appendback_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.c
int
logpb_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.c
static 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.c
static int
logpb_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.c
static int
logpb_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.c
static void
logpb_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.c
static void
logpb_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.c
static void
logpb_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). elseassert_release(false)다. 모두 SUCCESS로 끝난다.

불변식 — 레코드 브래킷. logpb_start_append(IN_PROGRESS)부터 logpb_end_append(SUCCESS)까지 정확히 레코드 하나가 쓰기 중 상태다. IN_PROGRESS를 보는 강제 flush는 불완전한 레코드를 잡은 것임을 안다. SUCCESS는 페이지를 flush해도 안전하다는 의미다. 이 브래킷이 깨지면 절반만 쓴 레코드가 표시 없이 디스크에 도달한다.

  1. 잠금 두 개, 역할 두 가지. prior_lsa_mutex는 appender들 사이를 직렬화하고, LOG_CS는 appender와 단일 writer 사이를 직렬화한다.
  2. Detach 후 drain. 뮤텍스 아래에서 header/tail/list_size를 초기화하고 뮤텍스를 해제한 다음 복사한다.
  3. LSN 순서, 즉시 해제. 각 노드는 logpb_append_next_record로 복사된 직후 세그먼트와 함께 해제된다.
  4. 세 개의 assert가 체인을 증명한다. back_lsa==prev_lsa, forw_lsa==append_lsa, start_lsa==append_lsa — 이 중 하나가 틀리면 fatal 종료다.
  5. 커서는 정직하게 유지된다. logpb_append_data는 복사한 바이트만큼 정확하게 append_lsa.offset을 전진시킨다.
  6. Dirty, 아직 flush 아님. logpb_set_dirtyLOG_BUFFER::dirty만 뒤집는다. flush/checksum/WAL은 Chapter 7에서 처리한다.
  7. 경계 넘기는 미룬다. 모든 logpb_next_append_page 호출은 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.offsetLOGAREA_SIZE와 비교한다(정렬/전진 산술은 Chapter 4 / Chapter 5 내용이다). LOG_APPEND_ALIGN은 조각 이후 DOUBLE_ALIGNMENT로 올림한 offset이 한계에 도달하면 넘기를 수행한다. LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT(length)offset + length가 넘칠 경우 쓰기 이전에 넘겨서 조각이 다음 페이지에 온전히 들어가도록 한다.

이 “이후 vs 이전” 분리가 logpb_next_append_pagecurrent_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.c
void
logpb_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_ALIGNLOG_SET_DIRTY로 호출)을 실행한다. 이것이 지켜지지 않으면 꽉 찬 페이지가 toflush[]에 dirty 표시 없이 들어가고 flusher가 이를 건너뛰어 commit된 레코드가 유실된다.

log_append_info는 appender의 고정 커서이고, log_page / log_hdrpage는 물리 페이지 레이아웃이며, log_flush_info는 flusher로 넘기는 대기열이다.

log_append_info (log_append.hpp) — 전역 하나, log_Gl.append.

필드역할존재 이유
vdesactive log의 볼륨 디스크립터 (fd)결국 fileio_write의 대상
nxio_lsaatomic<LOG_LSA>: 아직 디스크에 없는 최소 LSAWAL 경계; fetch 헬퍼(6.5)가 재조정, Chapter 7이 읽음
prev_lsa마지막으로 append된 레코드의 LSAlogpb_start_appendback_lsa == prev_lsa 확인; 논리값이라 경계 넘기 후에도 그대로 유지됨 (6.7)
log_pgptr현재 고정된 append 페이지경계 넘기가 null로 만든 후 재조정하는 포인터
appending_page_tde_encryptedappend 중간에 생성되는 페이지도 TDE 암호화 필요레코드의 암호화 결정을 레코드 중간에 새로 생성된 페이지에 전파 (6.6)

log_hdrpage (log_storage.hpp) — 모든 log 페이지 앞에 있는 헤더.

필드역할존재 이유
logical_pageidLOG_PAGEID: 무한 log에서 페이지의 주소독자/flusher가 프레임이 어떤 논리 페이지인지 알게 함
offsetPGLENGTH: 이 페이지에서 첫 번째 전체 레코드의 바이트 offset이전 페이지가 손상된 경우 recovery 시 salvage 앵커
flagsshort 비트 필드; 현재는 TDE 비트만 사용LOG_HDRPAGE_FLAG_ENCRYPTED_AES/_ARIA 보유; logpb_set_tde_algorithm으로 설정
checksumint: 페이지의 CRC32일관성 확인; 생성 시 memset 쓰레기값이고, logpb_writev_append_pagesfileio_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_toflushtoflush의 용량대기열이 찰 때 flush를 강제하는 임계값
num_toflush대기 중인 페이지 수경계 넘기마다 flush_mutex 아래에서 증가
toflushLOG_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가 조용히 손상된다.

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_pagenxio_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[] 부기를 생략한다는 점이다. 나머지 두 함수는 이 처리를 모두 수행한다.

암호화 결정은 레코드에 속하지만, 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_algorithmhdr.flags에 쓴다(암호화 마스크를 지우고 알고리즘 비트를 OR한다). logpb_locate_page가 생성 시 hdr.flags를 0으로 설정하기 때문에(6.4 단계 3), 새 페이지는 처음에 암호화되지 않은 상태이고 (H)가 이를 다시 기록한다. 뒤따르는 logpb_set_dirty가 없으면 이 비트가 유실될 수 있다. logpb_start_appendLOG_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를 확인하므로, 플래그가 없는 페이지는 조용히 암호화를 건너뛴다.

log_Gl.append.prev_lsa는 논리값이라 페이지에 상대적이지 않다. 그래서 logpb_next_append_page는 이 값을 건드리지 않는다. prev_lsa는 새로 넘어간 페이지에서 레코드가 시작되더라도 마지막으로 append된 레코드를 계속 가리킨다. 덕분에 logpb_start_appendheader->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 임계값을 기준으로 경계를 계산하므로 예약된 주소와 실체화된 프레임이 절대 어긋나지 않는다.

  1. 스트림 중간 경계 넘기는 함수 하나가 담당한다. logpb_next_append_page는 꽉 찬 페이지를 선택적으로 봉인하고, log_pgptr를 null로 만들고, append_lsa.pageid를 전진시키고, 새 프레임을 생성하고, 페이지를 대기열에 넣는다. 이 모든 것이 write-held LOG_CS 아래에서 이루어진다.
  2. 경계 넘기 시 enqueue되는 페이지는 꽉 찬 페이지가 아니라 페이지다. 모든 페이지는 자신이 태어날 때 정확히 한 번 toflush[]에 들어간다.
  3. NEW_PAGE 생성은 헤더 필드 세 개를 쓸 뿐, checksum은 쓰지 않는다. logpb_locate_pagelogical_pageid, offset = NULL_OFFSET, flags = 0을 설정하고 본문을 0xff로 memset한다. checksumfileio_write 직전 logpb_set_page_checksum이 계산한다.
  4. 생성 시 flags = 0이기 때문에 TDE를 다시 기록해야 한다. appending_page_tde_encrypted를 건드리는 지점은 세 군데다. logpb_append_next_record에서 설정/초기화하고, logpb_next_append_page (H)와 logpb_start_append에서 재기록하여 암호화된 레코드가 페이지 경계를 넘어도 암호화 상태를 유지한다.
  5. 임계값 flush는 flush_mutex 아래에서 결정되고, 밖에서 실행된다. num_toflush >= max_toflushneed_flush를 설정하고, logpb_flush_all_append_pages는 mutex 해제 이후에 실행된다.
  6. logpb_fetch_start_append_page vs _new. 전자는 NEW_PAGE/OLD_PAGE를 선택하고, 오래된 페이지를 폐기하고, enqueue하고, nxio_lsa를 재고정한다. _new는 항상 NEW_PAGE이며 자체적으로 flush를 관리하는 호출자를 위해 enqueue/임계값 처리를 생략한다.
  7. 경계 넘기는 Chapter 4와 짝을 이루는 버퍼 측 동작이다. prior 측은 바이트가 존재하기 전에 꼬리를 넘어 주소 공간을 예약하고, 이 챕터는 그 주소에 해당하는 프레임을 가져온다. 둘 다 LOGAREA_SIZE를 기준으로 삼기 때문에 예약 주소와 실체화된 프레임이 항상 일치한다.

이 장은 하나의 질문에 답한다. 더티 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_lsanxio_lsa.load ()이고 set_nxio_lsanxio_lsa.store ()이다. atomicity 자체가 계약이다.

log_flush_info{ int max_toflush; int num_toflush; LOG_PAGE **toflush; pthread_mutex_t flush_mutex; }를 보유한다(flush_mutexSERVER_MODE에서만 존재).

필드역할존재 이유
max_toflush배열 용량. num_toflush == max_toflush가 되면 버퍼가 꽉 찬 것으로 간주하며 log_buffer_full_count가 증가한다.배치 크기를 제한한다. 목록이 가득 찬 이벤트는 6장의 partial-append 경로를 유발한다.
num_toflush대기 중인 page 수. < 1이면 flush할 대상이 없다.루프 경계값이며, flush 후 0으로 리셋되고 live append page로 다시 채워진다.
toflushpageid 오름차순으로 정렬된 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_pageidactive log 볼륨 내 물리 offset.쓰기 대상은 phy_pageid + i이며, 연속성 판단에는 pageid+1이 아니라 phy_pageid+1이 필요하다.
dirtyflush되지 않은 변경 사항이 있음을 나타낸다.스캔의 주요 필터이며, 쓰기 성공 직후에만 지워진다.
logpagepage 바이트 전체(헤더 + 영역).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_pagesfileio_synchronize가 반환된 후, 그리고 nxio_lsa page가 마지막으로 기록된 후에만 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 = falsereturn 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 — 정상 경우. eof record를 만들고 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.c
if (!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.c
if (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_SUCCESSset_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.c
if (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 오류로 처리된다.

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.c
if (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_intervallooper 주기와 on-demand wakeup () 호출로 결정된다. companion 문서는 배치/지연 간 트레이드오프를 미해결 항목으로 표시한다. 이 장은 메커니즘을 기록할 뿐이며 튜닝 기준은 다루지 않는다.

7.6 flush daemon과 group-commit producer 측

섹션 제목: “7.6 flush daemon과 group-commit producer 측”

daemon은 log_get_log_group_commit_intervallooper 주기로 사용하는 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.c
if (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_walLOG_CS 바깥과 안쪽에서 두 번 확인하는 이유는, 이미 영속된 경우의 불필요한 critical section 진입과 concurrent committer가 nxio_lsa를 전진시킨 경우의 중복 flush를 모두 피하기 위해서다.

  1. nxio_lsa는 단 하나의 내구성 워터마크다. log_append_info에 atomic으로 선언되며 아직 기록되지 않은 최솟값 LSA를 나타낸다. “이 commit은 영속되었는가?”와 “이 data 쓰기 전에 flush해야 하는가?”를 동시에 답한다. fileio_synchronize가 성공한 후에만 전진한다.
  2. logpb_flush_all_append_pagesnxio_lsa page를 마지막에 flush한다. 2단계 스캔(클린 page 건너뛰기, 연속 더티 page 수집)으로 인접 page를 배치로 묶고, 워터마크 page만 별도로 마지막에 기록한다. 덕분에 새로운 end-of-log는 이전 page들이 디스크에 올라가기 전에는 절대 검증되지 않는다.
  3. flush_mutex는 flush 본문 전체를 커버한다. 스캔, nxio page 쓰기, fileio_synchronize가 모두 이 mutex를 쥔 채 실행된다. Phase-1의 num_toflush 확인만 별도의 짧은 lock으로 처리된다. group-commit 대기gc_cond/gc_mutex라는 별개의 lock을 사용한다.
  4. group commit은 하나의 fsync를 여러 committer에게 분산시킨다. waiter들은 gc_cond에서 블록하여 gc_mutex 아래에서 nxio_lsa를 재확인한다. daemon은 logpb_flush_pages_direct를 한 번 수행하고 broadcast하여 flush_lsanxio_lsa 이하인 모든 committer를 깨운다.
  5. 2×2 commit 매트릭스(async_commit × group_commit)는 wake-and-wait, just-wait, wake-and-return, just-return 중 하나를 선택한다. non-SERVER 환경과 daemon 미사용 경로는 직접 flush로 대체된다.
  6. WAL은 read-side에서 logpb_flush_log_for_wal로 강제된다. LOG_CS 주변에서 logpb_need_wal을 두 번 확인하는 방식으로 구현되며, 사후 조건은 어떠한 data page 쓰기 전에도 log가 요청된 LSA까지 영속되었음을 assert로 보장한다.
  7. group commit 윈도우 정책은 미해결 질문으로 남아 있다 (companion에서 이어짐). 메커니즘은 daemon의 looper 인터벌과 on-demand wakeup ()이지만, 배치/지연 간 튜닝 기준은 여기서 확정하지 않는다.

이 장이 답하는 핵심 질문은 하나다. 트랜잭션 경계 레코드가 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.hpp
struct log_rec_donetime
{
INT64 at_time; /* Database creation time. For safety reasons */
};
필드역할존재 이유
at_timelog_append_donetime_internal에서 캡처한 wall-clock time(NULL).완료 시각을 포렌식 용도로 기록한다. 주석의 “Database creation time”은 시대착오적 표현이며, 실제로는 종료 시각을 담는다. commit 프로토콜은 이 값을 다시 읽지 않는다.

불변식 — donetime 레코드의 LSA 자체가 commit 지점이다. 이 레코드는 다른 상태를 전혀 담지 않으므로, 내구성은 “이 LSA를 담은 페이지가 디스크에 있다”는 사실로 환원된다. §8.4의 모든 로직은 클라이언트에게 commit 성공을 알리기 전에 바로 그 조건을 만족시키기 위한 것이다.

제네릭 헤더(Chapter 1에서 전체 설명)는 수정 없이 재사용된다. 경계 레코드에서 특별한 의미를 갖는 필드는 typeprev_tranlsa뿐이다.

// log_rec_header — src/transaction/log_record.hpp
struct 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로부터 할당된다.
typeLOG_COMMIT, LOG_ABORT, 또는 postpone 작업이 남아 있을 때 LOG_COMMIT_WITH_POSTPONE.recovery 디스패치는 이 값을 기반으로 “이 trid는 완료됨 — undo 금지”를 판단한다.
back_lsa / forw_lsa데이터 레코드와 마찬가지로 prior-list 기계가 할당하는 물리 순서 링크.analysis 패스가 경계 레코드를 지나 스캔할 수 있게 한다.
tridcommit/abort 중인 트랜잭션의 id.recovery는 trid를 기준으로 레코드를 그룹화한다.

log_tdes — 트랜잭션 디스크립터(경계 관련 필드)

섹션 제목: “log_tdes — 트랜잭션 디스크립터(경계 관련 필드)”

log_tdes는 덩치가 크다. 여기서는 commit/abort 경로가 읽거나 쓰는 필드만 다룬다. 전체 struct는 log_impl.h에 있다.

// log_tdes (excerpt) — src/transaction/log_impl.h
struct 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에서의 역할
stateACTIVE -> WILL_COMMIT -> (…_WITH_POSTPONE) -> COMMITTED.어떤 rollback보다 먼저 바로 ABORTED로.
tail_lsaNULL이면 아무것도 건드리지 않은 것 -> donetime 생략(§8.3, §8.5); 아니면 레코드가 연결되는 체인 tail.동일한 게이트.
undo_nxlsaNULL로 초기화해 WILL_COMMIT 중 발생하는 checkpoint가 낡은 커서를 보지 않도록 한다.log_rollbackprev_tranlsa를 따라 역방향으로 순회하는 rollback 커서.
posp_nxlsaNULL이 아니면 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.c
if (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_clearlogtb_complete_mvcc(둘 다 로그를 기록함)는 tdes->state = TRAN_UNACTIVE_WILL_COMMIT 이전에 실행된다.

// log_commit_local — src/transaction/log_manager.c
tx_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_postponeLOG_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.c
if (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_postponestate = 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_loglog_append_donetime_internal 위에 얹힌 얇은 래퍼다. log_append_donetime_internal은 commit과 abort 모두가 donetime 레코드를 구성하는 단일 지점이다.

// log_append_donetime_internal — src/transaction/log_manager.c
node = 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.c
if (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_commitlog_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.c
if (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 */

분기 팬아웃:

  1. tail_lsa NULL. EOT 레코드 없이 상태를 설정하고, trid를 재활용하거나(LOG_NEED_NEWTRID) 강제 초기화한다(logtb_clear_tdes).
  2. tail_lsa non-NULL, abort. abort 레코드를 기록하고 §8.4에 따라 force한 뒤 상태를 설정한다.
  3. tail_lsa non-NULL, commit. 레코드가 이미 기록되었고 상태가 TRAN_UNACTIVE_COMMITTED임을 단언한다.
  4. MVCC 차단 해제(데이터 경로). unlock_global_oldest_visible_mvccid는 항상 실행되고, reset_transaction_lowest_active는 commit에서만 실행된다(cubrid-mvcc.md).
  5. 다음 trid. logtb_get_new_tran_id는 해당 인덱스에 새로운 trid를 재활용한다. donetime 레코드 자체가 CUBRID의 EOT 마커이며, 별도의 타입이 존재하지 않는다.
  6. 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_clearlogtb_complete_mvcc가 기록하는 레코드들은 상태가 WILL_COMMIT으로 이동하기 전에 기록되며, tail_lsa가 비어 있으면 레코드 없이 read-only commit으로 단락된다.

8.6 log_abortlog_abort_local — 경계 이전의 undo

섹션 제목: “8.6 log_abort와 log_abort_local — 경계 이전의 undo”

log_abortlog_commit의 진입 검증을 미러링하되 두 가지 추가 가드를 붙이고, log_abort_local -> log_complete로 분기한다.

// log_abort (excerpt) — src/transaction/log_manager.c
if (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 commit
state = 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_locallog_commit_local과 다른 점은 순서에 있다. 상태를 TRAN_UNACTIVE_ABORTED먼저 설정한 뒤 작업을 수행한다.

// log_abort_local — src/transaction/log_manager.c
tdes->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_localretain_lock을 무시한다. abort는 항상 lock_unlock_all을 호출한다.

8.6.1 log_rollbackprev_tranlsa를 역방향으로 걷기

섹션 제목: “8.6.1 log_rollback — prev_tranlsa를 역방향으로 걷기”

log_rollbacktdes->undo_nxlsa에서 출발해 체인을 역방향으로 걸으며, 각 undo 이미지를 재적용한다. 레코드 타입별 CLR 생성은 Chapter 9에서 다루며, 여기서 중요한 분기는 커서 규율이다.

// log_rollback (control skeleton) — src/transaction/log_manager.c
LSA_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_tranlsatdes->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_tdeslog_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_tasklog_abort_task_execute를 큐에 넣고, 이 래퍼가 log_abort_by_tdes(&thread_ref, &tdes)를 호출한다.

// log_abort_all_active_transaction (server-mode essence) — src/transaction/log_manager.c
if (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를 동기적으로 호출한다.

  1. 경계 레코드는 전체 파이프라인을 재사용한다 — 데이터 레코드와 동일하게 구성/attach되며(Chapter 3-4), 단일 필드 log_rec_donetimeLSA가 내구적 commit 지점이다.
  2. log_commit은 분기하고 log_commit_local이 실제 작업을 한다 — postpone, append, unlock, force가 차례로 이루어지며, log_complete는 레코드가 이미 기록된 상태에서(LOG_ALREADY_WROTE_EOT_LOG) 상태만 최종 확정한다.
  3. 순서 규율은 commit 중 checkpoint로부터 보호한다 — 로그를 기록하는 모든 작업은 TRAN_UNACTIVE_WILL_COMMIT 이전에 실행된다.
  4. commit은 force하고 abort는 대체로 하지 않는다logpb_flush_pages는 commit에서 항상 실행되며(group-commit, Chapter 7), abort에서는 재시작된 서버에서 checkpoint가 진행 중일 때만 실행된다.
  5. abort는 상태를 먼저 설정한 뒤 undo한다log_rollback은 CLR이 체인에 재진입하지 않도록 undo 전에 커서를 전진시킨다.
  6. retain_lock은 commit 전용 옵션이다 — abort는 항상 unlock한다.
  7. 재시작 변형은 재바인딩할 뿐 재구현하지 않는다log_abort_by_tdestran_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)이 어떻게 변하는가에 있다.

log_sysop_start는 로그 레코드를 아무것도 기록하지 않는다. 대신 트랜잭션 디스크립터의 인메모리 스택에 프레임 하나를 푸시한다. 아래 표는 sysop/postpone와 직접 관련된 log_tdes 필드만 정리한 것이며, 전체 약 82개 필드로 구성된 struct는 Chapter 2에서 확인할 수 있다.

필드역할존재 이유
topops (LOG_TOPOPS_STACK)활성 sysop의 중첩 스택last는 현재 깊이, max는 할당된 크기
topop_lsa가장 최근 sysop의 부모 LSAappender가 “sysop 안에 있는가”를 빠르게 확인하기 위한 프로브
tail_lsa이 트랜잭션의 마지막으로 추가된 레코드 LSAsysop end가 “변경 없음”을 감지할 때 비교하는 상한선
undo_nxlsa다음으로 undo할 레코드CLR이 되감아서 이미 undo된 레코드를 건너뛰게 한다
posp_nxlsa첫 번째 트랜잭션 레벨 postpone 레코드어떤 sysop 밖에서 추가된 LOG_POSTPONE가 여기에 씨앗을 뿌린다
savept_lsa마지막 savepoint의 LSAsavepoint 체인; 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 + LSAdo_postpone가 메모리에서 재실행할 수 있게 한다
rcv.sysop_start_postpone_lsa진행 중인 sysop postpone의 복구 앵커크래시 후 sysop의 postpone 단계를 재개하기 위한 기준점
rcv.tran_start_postpone_lsa트랜잭션 레벨 postpone의 복구 앵커트랜잭션 postpone 단계 재개를 위한 기준점
rcv.atomic_sysop_start_lsaatomic sysop의 복구 앵커중단된 atomic sysop을 단일 단위로 롤백하는 데 사용

(rcv.* 멤버는 내장된 log_rcv_tdes 안에 있다.) 스택의 각 프레임은 두 개의 LSA를 갖는 log_topops_addresses이며, 세 개의 접근자 매크로를 통해 읽는다.

// log_topops_addresses -- src/transaction/log_impl.h
struct 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_internallog_sysop_abort에서 강제한다.

9.2 log_sysop_startlog_sysop_start_atomic

섹션 제목: “9.2 log_sysop_start와 log_sysop_start_atomic”
// log_sysop_start -- src/transaction/log_manager.c
if (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 == NULLER_LOG_UNKNOWN_TRANINDEX 치명 오류 조기 반환, (2) realloc 실패 시 OOM 언락 후 푸시 없이 반환, (3) VACUUM 진단 로그만 처리, (4) 정상 경로는 tail_lsalastparent_lsa에 저장하고 posp_lsa를 null로 초기화한다.

log_sysop_start_atomic은 이 함수를 감싸고 나서, 복구 시 전체 atomic sysop을 하나의 단위로 롤백할 수 있도록 LOG_SYSOP_ATOMIC_START 마커가 하나 존재하는지 확인한다.

// log_sysop_start_atomic -- src/transaction/log_manager.c
log_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.hpp
struct 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_lsasysop 본문이 시작되는 위치복구 undo가 여기서 멈춘다. 프레임에서 복사된다
prv_topresult_lsa이전 partial commit/abort의 LSA복구가 top result를 역방향으로 탐색할 수 있도록 체인을 잇는다
type유니온의 어떤 멤버가 유효한가append와 복구의 디스패치 키
vfid영향받은 페이지가 속한 파일TDE 키 조회; MVCC vacuum 정보로도 쓰인다
undoLOGICAL_UNDOLOG_REC_UNDO 페이로드logical undo 재실행을 위한 rcvindex + 길이
mvcc_undoLOGICAL_MVCC_UNDOLOG_REC_MVCC_UNDO 페이로드mvccidvacuum_info를 추가한다
compensate_lsaLOGICAL_COMPENSATE의 undo 건너뛰기 대상이 logical compensation 이후의 next-undo LSA
run_postpone.postpone_lsa원본 LOG_POSTPONE의 LSArun-postpone을 그 원천에 연결한다
run_postpone.is_sysop_postponesysop 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_UNDOundolog_sysop_end_logical_undo (non-MVCC)
LOG_SYSOP_END_LOGICAL_MVCC_UNDOmvcc_undolog_sysop_end_logical_undo (MVCC)
LOG_SYSOP_END_LOGICAL_COMPENSATEcompensate_lsalog_sysop_end_logical_compensate
LOG_SYSOP_END_LOGICAL_RUN_POSTPONErun_postponelog_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.c
assert (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_postponeis_sysop_postpone를 commit_internal이 state로부터 직접 파생하도록 남겨 둔다.

abort는 commit_internal을 거치지 않는다. 직접 롤백하고 ABORT end를 찍는다.

// log_sysop_abort -- src/transaction/log_manager.c
if (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.c
node = 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_postponeLOG_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.c
if (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.c
LSA_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_postponeWILL_COMMIT 또는 *_COMMITTED_WITH_POSTPONE 상태인지 assert하고, 세 필드를 채워 추가하며 페이지 LSA를 설정한다. 이로써 두 번째 복구 패스에서도 이 액션이 멱등성을 갖는다.

9.8 Compensation: log_append_compensate_internalundo_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.c
node = 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_nxlsaprev_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 단위로 같은 건너뛰기를 수행한다.

log_append_savepointLOG_SAVEPOINTlog_rec_savept 헤더와 함께 체인으로 연결한다(log_record.hpp).

필드역할존재 이유
prv_savept”이전 savepoint 레코드”의 LSAlog_get_savepoint_lsa가 이름으로 역방향 탐색할 수 있도록 단방향 연결 리스트를 구성한다
length”Savepoint 이름” 길이 (이름은 레코드 뒤에 붙는다)가변 길이 이름 복사의 경계를 정한다
// log_append_savepoint -- src/transaction/log_manager.c
if (!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을 만들어 처리한다. 실제 본문에 들어가기 전에 다섯 가지 가드가 실행된다.

  1. tdes == NULLER_LOG_UNKNOWN_TRANINDEX, TRAN_UNACTIVE_UNKNOWN 반환.
  2. LOG_HAS_LOGGING_BEEN_IGNORED ()ER_LOG_CORRUPTED_DB_DUE_NOLOGGING, 현재 state 반환.
  3. !LOG_ISTRAN_ACTIVE → 현재 state를 조용히 반환.
  4. 이름이 NULL이거나 savepoint를 찾을 수 없는 경우 → ER_LOG_UNKNOWN_SAVEPOINT, TRAN_UNACTIVE_UNKNOWN 반환.
  5. 매달린 sysop이 있는 경우(topops.last >= 0) → 경고 + assert(false), log_sysop_attach_to_outer로 전부 비운다.
// log_abort_partial -- src/transaction/log_manager.c
if (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.c
if (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의 효과는 부모의 것으로 통합된다.

  1. Sysop은 스택 프레임으로 시작하며 로그 레코드가 아니다. log_sysop_starttopops에 푸시하고 tail_lsalastparent_lsa에 저장한다. 디스크에 남는 첫 번째 흔적은 end 레코드(또는 atomic-start 마커)다. 부모-LSA 불변식(§9.1)이 end 함수가 빈 sysop을 감지하는 수단이다.
  2. log_sysop_commit_internal은 여섯 개의 type 기반 분기를 갖는 단일 허브다. log_rec_sysop_end 유니온은 type에 따라 재해석된다. 이 함수는 분기-상태 조합을 검증하고, postpone를 실행하고, end를 추가하며, tail_topresult_lsa를 체인으로 잇는다.
  3. Abort는 상태 스왑 후 롤백-마킹이다. log_sysop_abortTRAN_UNACTIVE_ABORTED로 설정해 log_rollback이 CLR을 기록하게 하고, LOG_SYSOP_END_ABORT를 추가한 뒤 외부 상태를 복원한다.
  4. Postpone는 redo를 commit 이후 재실행으로 미루며 앵커는 한 번만 설정된다. 첫 번째 LOG_POSTPONEposp_lsa(sysop) 또는 posp_nxlsa(tran)에 씨앗을 뿌린다. 캐시는 메모리에서, log_do_postpone는 로그에서 재실행하며, 시작 마커를 만나면 중단하고 LOG_POSTPONE만 재실행한다.
  5. log_run_postpone_opLOG_RUN_POSTPONEref_lsa 역방향 포인터를 통해 postpone를 멱등적으로 만든다. 페이지 경계를 넘는 malloc 실패는 치명 오류다.
  6. Compensation은 redo-only이며 undo 커서를 되감는다. log_append_compensate_internal은 보상된 레코드를 건너뛰는 undo_nxlsa를 가진 CLR을 기록하고 tdes->undo_nxlsa를 재설정한다.
  7. 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_PAGELOG_HDRPAGE hdrchar area[1]로 구성된다.

필드역할존재 이유
logical_pageid무한 로그 내 논리 페이지 ID물리적 슬롯과 독립된 식별자. header 페이지는 항상 -9
offset이 페이지에서 첫 번째 레코드의 오프셋이전 페이지가 손상되어 archive에 없을 때의 복구 앵커
flagsTDE 플래그 (..._AES/_ARIA)메모리에서 벗어나기 전에 암호화해야 하는 페이지를 표시. header 페이지는 0
checksum샘플 바이트에 대한 CRC32읽기 시 손상 감지 (10.6절)

LOG_HEADER — active 로그 마스터 레코드. 페이지 -9의 데이터 영역에 위치하며 log_Gl.hdr로 미러링된다. 모든 멤버를 아래에 나열한다.

필드역할존재 이유
magicfile(1) 매직로그 파일이 아닌 파일에 대한 가드
dummy / dummy3 / dummy4정렬 패딩8바이트 정렬
db_creationDB 생성 시각로그를 DB에 연결. LOG_ARV_HEADER로 복사됨
vol_creationactive 볼륨 생성 시각진단 / 순서 확인
db_release / db_compatibility릴리스 문자열, 호환성 부동소수점호환되지 않는 빌드/버전 거부
db_iopagesize / db_logpagesize생성 시 페이지 크기로그가 기대하는 크기로 DB 실행
is_shutdown정상 종료 플래그Recovery: dismount가 정상적이었는지 여부
next_trid다음 트랜잭션 IDreplay를 위해 LOG_ARV_HEADER로 복사됨
mvcc_next_id다음 MVCC IDMVCC 할당 상한선
avg_ntrans / avg_nlocks크기 추정치트랜잭션/잠금 테이블 사전 크기 조정
npagesactive 페이지 수 (header 제외)active 볼륨 크기 / archive 범위 조정
db_charsetDB 문자셋 ID마운트 시 문자셋 검사
was_copied복사된 DB 플래그복사본 대 원본 구분
fpageidactive 물리 슬롯 1의 논리 페이지 IDLOG_ARV_HEADER.fpageid의 active 측 대응
append_lsa현재 append 위치실제 로그의 상한선
chkpt_lsaRecovery가 재생을 시작하는 최소 LSARecovery 시작점. 여기서 durable (10.5절)
nxarv_pageid다음으로 archive할 논리 페이지active/archive 경계 (10.2절)
nxarv_phy_pageidnxarv_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_timeHA 상태, 복사 상태, 승격 시각복제 / HA
eof_lsaLOG_END_OF_LOG의 LSAdurable 로그 끝. 여기서 durable (10.5절)
smallest_lsa_at_last_chkpt마지막 checkpoint 시의 가장 오래된 dirty LSARecovery/vacuum 소급 범위 제한
mvcc_op_log_lsa마지막 MVCC 연산의 LSAVacuum MVCC 앵커
oldest_visible_mvccid / newest_block_mvccid가장 오래된 가시 MVCCID, 가장 최신 블록 MVCCIDVacuum 가시성 / 블록 경계
db_restore_time마지막 복원 시각복원 기록 관리
mark_will_del삭제 예정 표시DB 삭제 기록 관리
does_block_need_vacuum블록 vacuum 필요 여부Vacuum 스케줄링
was_active_log_resetactive 로그 초기화 여부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 볼륨 하나당 하나씩.

필드역할존재 이유
magicCUBRID_MAGIC_LOG_ARCHIVE잘못된 파일을 archive로 마운트하는 것에 대한 가드
dummy패딩정렬
db_creationlog_Gl.hdr.db_creation에서 복사archive를 DB에 연결
vol_creation기록 시 time(NULL)진단 / 순서 확인
next_tridlog_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.pageider_log_debug + return)는 빈 범위 요청을 거부한다. 이후 logpb_flush_all_append_pages가 강제로 실행된다. header는 자기 서술적으로 구성되는데 (db_creation/next_trid/fpageidlog_Gl.hdr에서 복사), 퇴화된 범위에서 음수 npages가 나오지 않도록 last_pageid를 아래와 같이 클램핑한다.

// logpb_archive_active_log -- src/transaction/log_page_buffer.c
last_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[] 페이지를 복사한다. 이 과정에서 커서 pageidtoflush[] 내 다음 bufptr->pageid를 세 가지 분기로 조정한다.

// logpb_write_toflush_pages_to_archive -- src/transaction/log_page_buffer.c
if (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_synchronizePRM_ID_PB_SYNC_ON_NFLUSH 페이지마다 한 번씩 실행된다. 쓰기 오류가 발생하면 임시 볼륨을 dismount하고 bg archiving을 포기한다. 이 경우 10.2절의 logpb_archive_active_logfileio_format으로 대체한다.

10.4 remove daemon — 오래된 archive의 게이팅 삭제

섹션 제목: “10.4 remove daemon — 오래된 archive의 게이팅 삭제”

삭제는 핫 경로에서 발생하지 않는다. 서버 환경에서 logpb_archive_active_log는 daemon을 깨우기만 한다 (log_wakeup_remove_log_archive_daemonwakeup()을 비동기적으로 호출). log_remove_log_archive_daemon_task는 주기적으로도 실행된다 (compute_periodPRM_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.c
if (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.c
log_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_errorlogpb_fatal_error_internalneed_flush = true로 호출한다. flush 자체가 안전하지 않을 때는 logpb_fatal_error_exit_immediately_wo_flushfalse를 전달한다.

// logpb_fatal_error_internal -- src/transaction/log_page_buffer.c
if (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를 필요로 하는 조건이 소스 주석으로만 문서화되어 있음).

  1. 세 가지 중첩된 header 구조체: 페이지당 LOG_HDRPAGE, 마스터 레코드로서 LOG_HEADER (페이지 -9), archive당 LOG_ARV_HEADER. db_creation/next_trid는 앞의 것에서 복사된다.
  2. nxarv_pageid/nxarv_phy_pageid가 archive 경계이며, logpb_archive_active_log에서 한 단위로 증가하고 flush된다 (was_active_log_reset도 여기서 초기화).
  3. Archiving은 전체 append flush를 강제하고, [nxarv_pageid .. prev_lsa.pageid-1]을 저장된 그대로 복사하며, I/O 오류를 치명적으로 처리한다.
  4. 삭제는 게이팅된다: logpb_remove_archive_logs_exceed_limit은 크래시 recovery, vacuum, CDC, flashback, HA 하한선에 대한 MIN() 체인으로 창을 클램핑한다.
  5. logpb_flush_headerchkpt_lsa/eof_lsa/archive 기록의 단일 durability 지점이며, LOG_CS 쓰기 모드 아래 flags = 0으로 동작한다.
  6. 경계 레코드: LOG_END_OF_LOGappend_lsa를 앞당기지 않고 제자리에 추가되고, dummy는 페이지를 패딩하며, 샘플링된 CRC32 checksum이 logpb_page_check_corruption을 구동한다.
  7. Fatal 경로: logpb_fatal_error_internalin_fatal 가드를 사용하고, prev_lsa까지만 flush한 뒤 exit/abort한다.

아래 줄 번호는 2026-06-08 기준으로 관찰된 값이다. 심볼이 정식 앵커이며 줄 번호는 시간이 지남에 따라 부정확해질 수 있는 힌트다.

심볼파일
LOG_PAGESIZEsrc/storage/storage_common.h99
log_Zip_supportsrc/transaction/log_append.cpp40
log_Zip_min_size_to_compresssrc/transaction/log_append.cpp41
log_append_info::get_nxio_lsasrc/transaction/log_append.cpp106
log_append_info::set_nxio_lsasrc/transaction/log_append.cpp112
log_prior_lsa_info::log_prior_lsa_infosrc/transaction/log_append.cpp117
LOG_RESET_APPEND_LSAsrc/transaction/log_append.cpp128
LOG_RESET_PREV_LSAsrc/transaction/log_append.cpp136
LOG_APPEND_PTRsrc/transaction/log_append.cpp145
log_append_init_zipsrc/transaction/log_append.cpp185
log_append_final_zipsrc/transaction/log_append.cpp232
prior_lsa_alloc_and_copy_datasrc/transaction/log_append.cpp273
prior_lsa_alloc_and_copy_crumbssrc/transaction/log_append.cpp410
prior_lsa_copy_undo_data_to_nodesrc/transaction/log_append.cpp493
prior_lsa_copy_redo_data_to_nodesrc/transaction/log_append.cpp524
prior_lsa_gen_undoredo_record_from_crumbssrc/transaction/log_append.cpp651
prior_lsa_gen_recordsrc/transaction/log_append.cpp1217
prior_update_header_mvcc_infosrc/transaction/log_append.cpp1320
prior_lsa_next_record_internalsrc/transaction/log_append.cpp1357
commit_abort_lsasrc/transaction/log_append.cpp1485
prior_lsa_next_recordsrc/transaction/log_append.cpp1553
prior_lsa_next_record_with_locksrc/transaction/log_append.cpp1559
prior_set_tde_encryptedsrc/transaction/log_append.cpp1565
prior_is_tde_encryptedsrc/transaction/log_append.cpp1581
prior_lsa_start_appendsrc/transaction/log_append.cpp1593
prior_lsa_end_appendsrc/transaction/log_append.cpp1652
prior_lsa_append_datasrc/transaction/log_append.cpp1661
log_append_get_zip_undosrc/transaction/log_append.cpp1725
log_append_get_zip_redosrc/transaction/log_append.cpp1751
log_prior_lsa_append_alignsrc/transaction/log_append.cpp1892
log_prior_lsa_append_advance_when_doesnot_fitsrc/transaction/log_append.cpp1905
log_prior_lsa_append_add_alignsrc/transaction/log_append.cpp1917
log_crumbsrc/transaction/log_append.hpp46
log_data_addrsrc/transaction/log_append.hpp53
LOG_PRIOR_LSA_LOCKsrc/transaction/log_append.hpp66
log_append_infosrc/transaction/log_append.hpp73
log_prior_nodesrc/transaction/log_append.hpp91
log_prior_lsa_infosrc/transaction/log_append.hpp112
log_zip_allocsrc/transaction/log_compress.c237
log_zipsrc/transaction/log_compress.h53
log_global::log_globalsrc/transaction/log_global.c49
LOGAREA_SIZEsrc/transaction/log_impl.h121
log_setdirtysrc/transaction/log_impl.h305
log_flush_infosrc/transaction/log_impl.h322
log_topops_addressessrc/transaction/log_impl.h353
log_topops_stacksrc/transaction/log_impl.h362
log_rcv_tdessrc/transaction/log_impl.h458
log_tdessrc/transaction/log_impl.h475
log_globalsrc/transaction/log_impl.h671
log_lsasrc/transaction/log_lsa.hpp35
NULL_LSAsrc/transaction/log_lsa.hpp67
MAX_LSAsrc/transaction/log_lsa.hpp72
LSA_COPYsrc/transaction/log_lsa.hpp80
LSA_AS_ARGSsrc/transaction/log_lsa.hpp91
LOG_TDES_LAST_SYSOPsrc/transaction/log_manager.c199
LOG_TDES_LAST_SYSOP_PARENT_LSAsrc/transaction/log_manager.c200
LOG_TDES_LAST_SYSOP_POSP_LSAsrc/transaction/log_manager.c201
log_Flush_daemonsrc/transaction/log_manager.c363
log_create_internalsrc/transaction/log_manager.c827
log_initialize_internalsrc/transaction/log_manager.c1100
log_abort_by_tdessrc/transaction/log_manager.c1583
log_abort_all_active_transactionsrc/transaction/log_manager.c1608
log_finalsrc/transaction/log_manager.c1720
log_append_undoredo_datasrc/transaction/log_manager.c1893
log_append_undo_datasrc/transaction/log_manager.c1973
log_append_redo_datasrc/transaction/log_manager.c2035
log_append_undoredo_crumbssrc/transaction/log_manager.c2086
log_append_postponesrc/transaction/log_manager.c2719
log_append_run_postponesrc/transaction/log_manager.c2881
log_append_compensate_internalsrc/transaction/log_manager.c3047
log_append_savepointsrc/transaction/log_manager.c3365
log_sysop_startsrc/transaction/log_manager.c3599
log_sysop_start_atomicsrc/transaction/log_manager.c3665
log_sysop_commit_internalsrc/transaction/log_manager.c3825
log_sysop_commitsrc/transaction/log_manager.c3916
log_sysop_end_logical_undosrc/transaction/log_manager.c3941
log_sysop_end_logical_compensatesrc/transaction/log_manager.c3984
log_sysop_end_logical_run_postponesrc/transaction/log_manager.c4003
log_sysop_end_recovery_postponesrc/transaction/log_manager.c4024
log_sysop_abortsrc/transaction/log_manager.c4038
log_sysop_attach_to_outersrc/transaction/log_manager.c4097
log_append_commit_postponesrc/transaction/log_manager.c4384
log_append_sysop_start_postponesrc/transaction/log_manager.c4455
log_append_repl_info_and_commit_logsrc/transaction/log_manager.c4647
log_append_donetime_internalsrc/transaction/log_manager.c4679
log_change_tran_as_completedsrc/transaction/log_manager.c4722
log_append_commit_logsrc/transaction/log_manager.c4779
log_append_commit_log_with_locksrc/transaction/log_manager.c4802
log_append_abort_logsrc/transaction/log_manager.c4816
log_commit_localsrc/transaction/log_manager.c5159
log_abort_localsrc/transaction/log_manager.c5277
log_commitsrc/transaction/log_manager.c5352
log_abortsrc/transaction/log_manager.c5461
log_abort_partialsrc/transaction/log_manager.c5558
log_completesrc/transaction/log_manager.c5653
log_rollbacksrc/transaction/log_manager.c7664
log_tran_do_postponesrc/transaction/log_manager.c8156
log_sysop_do_postponesrc/transaction/log_manager.c8190
log_do_postponesrc/transaction/log_manager.c8237
log_run_postpone_opsrc/transaction/log_manager.c8481
log_wakeup_remove_log_archive_daemonsrc/transaction/log_manager.c10099
log_wakeup_log_flush_daemonsrc/transaction/log_manager.c10126
log_is_log_flush_daemon_availablesrc/transaction/log_manager.c10141
log_remove_log_archive_daemon_tasksrc/transaction/log_manager.c10185
log_flush_executesrc/transaction/log_manager.c10377
log_flush_daemon_initsrc/transaction/log_manager.c10493
log_abort_task_executesrc/transaction/log_manager.c10558
cdc_min_log_pageid_to_keepsrc/transaction/log_manager.c14021
LOG_IS_SYSTEM_OP_STARTEDsrc/transaction/log_manager.h59
LOGPB_HEADER_PAGE_IDsrc/transaction/log_page_buffer.c138
LOG_APPEND_ALIGNsrc/transaction/log_page_buffer.c164
LOG_APPEND_ADVANCE_WHEN_DOESNOT_FITsrc/transaction/log_page_buffer.c176
LOG_APPEND_ADVANCE_WHEN_DOESNOT_FITsrc/transaction/log_page_buffer.c177
LOG_APPEND_SETDIRTY_ADD_ALIGNsrc/transaction/log_page_buffer.c184
log_buffersrc/transaction/log_page_buffer.c192
log_buffersrc/transaction/log_page_buffer.c194
log_pb_global_datasrc/transaction/log_page_buffer.c244
logpb_get_log_buffersrc/transaction/log_page_buffer.c394
logpb_initialize_log_buffersrc/transaction/log_page_buffer.c425
logpb_compute_page_checksumsrc/transaction/log_page_buffer.c446
logpb_set_page_checksumsrc/transaction/log_page_buffer.c495
logpb_page_has_valid_checksumsrc/transaction/log_page_buffer.c523
logpb_initialize_poolsrc/transaction/log_page_buffer.c553
logpb_finalize_poolsrc/transaction/log_page_buffer.c672
logpb_create_pagesrc/transaction/log_page_buffer.c783
logpb_locate_pagesrc/transaction/log_page_buffer.c807
logpb_set_dirtysrc/transaction/log_page_buffer.c929
logpb_flush_headersrc/transaction/log_page_buffer.c1676
logpb_fetch_start_append_pagesrc/transaction/log_page_buffer.c2504
logpb_fetch_start_append_page_newsrc/transaction/log_page_buffer.c2586
logpb_next_append_pagesrc/transaction/log_page_buffer.c2630
logpb_writev_append_pagessrc/transaction/log_page_buffer.c2780
logpb_write_toflush_pages_to_archivesrc/transaction/log_page_buffer.c2868
logpb_append_next_recordsrc/transaction/log_page_buffer.c2981
logpb_append_prior_lsa_listsrc/transaction/log_page_buffer.c3040
prior_lsa_remove_prior_listsrc/transaction/log_page_buffer.c3084
logpb_prior_lsa_append_all_listsrc/transaction/log_page_buffer.c3106
logpb_flush_all_append_pagessrc/transaction/log_page_buffer.c3232
logpb_flush_pages_directsrc/transaction/log_page_buffer.c3952
logpb_flush_pagessrc/transaction/log_page_buffer.c3980
logpb_force_flush_pagessrc/transaction/log_page_buffer.c4096
logpb_force_flush_header_and_pagessrc/transaction/log_page_buffer.c4104
logpb_invalid_all_append_pagessrc/transaction/log_page_buffer.c4121
logpb_flush_log_for_walsrc/transaction/log_page_buffer.c4162
logpb_start_appendsrc/transaction/log_page_buffer.c4207
logpb_append_datasrc/transaction/log_page_buffer.c4290
logpb_append_crumbssrc/transaction/log_page_buffer.c4366
logpb_end_appendsrc/transaction/log_page_buffer.c4455
logpb_archive_active_logsrc/transaction/log_page_buffer.c5649
logpb_remove_archive_logs_exceed_limitsrc/transaction/log_page_buffer.c5991
logpb_fatal_errorsrc/transaction/log_page_buffer.c10607
logpb_fatal_error_exit_immediately_wo_flushsrc/transaction/log_page_buffer.c10618
logpb_fatal_error_internalsrc/transaction/log_page_buffer.c10629
logpb_initialize_flush_infosrc/transaction/log_page_buffer.c10878
logpb_finalize_flush_infosrc/transaction/log_page_buffer.c10912
logpb_need_walsrc/transaction/log_page_buffer.c11229
logpb_page_check_corruptionsrc/transaction/log_page_buffer.c11508
logpb_get_tde_algorithmsrc/transaction/log_page_buffer.c11565
logpb_set_tde_algorithmsrc/transaction/log_page_buffer.c11593
log_rectypesrc/transaction/log_record.hpp35
log_rec_headersrc/transaction/log_record.hpp146
log_datasrc/transaction/log_record.hpp157
log_rec_undoredosrc/transaction/log_record.hpp167
log_rec_undosrc/transaction/log_record.hpp176
log_rec_redosrc/transaction/log_record.hpp184
log_vacuum_infosrc/transaction/log_record.hpp192
log_rec_mvcc_undoredosrc/transaction/log_record.hpp202
log_rec_mvcc_undosrc/transaction/log_record.hpp211
log_rec_mvcc_redosrc/transaction/log_record.hpp220
log_rec_donetimesrc/transaction/log_record.hpp237
log_rec_compensatesrc/transaction/log_record.hpp262
log_rec_start_postponesrc/transaction/log_record.hpp271
log_sysop_end_typesrc/transaction/log_record.hpp285
log_rec_sysop_endsrc/transaction/log_record.hpp305
log_rec_sysop_start_postponesrc/transaction/log_record.hpp328
log_rec_run_postponesrc/transaction/log_record.hpp336
log_rec_saveptsrc/transaction/log_record.hpp380
LOG_GET_LOG_RECORD_HEADERsrc/transaction/log_record.hpp441
LOG_IS_MVCC_OP_RECORD_TYPEsrc/transaction/log_record.hpp463
LOG_HDRPAGE_FLAG_ENCRYPTED_MASKsrc/transaction/log_storage.hpp45
LOG_IS_PAGE_TDE_ENCRYPTEDsrc/transaction/log_storage.hpp47
LOGPB_HEADER_PAGE_IDsrc/transaction/log_storage.hpp51
log_hdrpagesrc/transaction/log_storage.hpp63
log_pagesrc/transaction/log_storage.hpp80
log_pagesrc/transaction/log_storage.hpp81
log_headersrc/transaction/log_storage.hpp113
log_arv_headersrc/transaction/log_storage.hpp231
logtb_get_new_tran_idsrc/transaction/log_tran_table.c1741
LOG_IS_MVCC_OPERATIONsrc/transaction/mvcc.h261
  • 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