콘텐츠로 이동

CUBRID Prior List — 코드 수준 심층 분석

이 문서의 위치: 상위 분석 문서인 cubrid-prior-list.md는 설계 의도와 이론적 배경을 다룬다. 이 문서는 모든 분기와 필드를 코드 수준에서 추적한다. 각 챕터는 독립적으로 읽을 수 있지만, 순서대로 읽으면 prior node 하나가 생성되어 LSA를 할당받고 로그 페이지 버퍼로 drain되는 전 과정을 따라가게 된다.

목차:

Ch제목상태
1자료구조 지도
2초기화 메모리와 두 개의 커서
3Non-Undoredo 레코드를 위해 Mutex 바깥에서 노드 구성하기
4압축과 Diff 인코딩을 적용한 Undoredo 노드 구성
5LSN 할당과 뮤텍스 하의 연결
6드레인 측에서의 리스트 분리
7노드 바이트를 권위 있는 로그 페이지에 복사하기
8디스크 플러시와 커밋 대기자 깨우기
9배압(Backpressure)과 자기 구제 소진(Self-Help Drain)
10특수 경로와 엣지 케이스

이 챕터가 답하는 독자 질문: prior list에 정확히 무엇이 큐잉되며, 큐 자체는 어떤 상태를 보유하는가? 이후 모든 챕터 — 빌드(Ch 3–4), 스탬프·스플라이스(Ch 5), 디태치(Ch 6), 페이지 복사(Ch 7) — 는 이 챕터에서 정의하는 여섯 개 구조체와 하나의 열거형을 조작한다. 모든 필드를 다루며, 누가 언제 쓰는지를 표시하고, 포인터 다이어그램으로 마무리한다.

producer/consumer 이론이나 Postgres/InnoDB/Oracle 비교는 반복하지 않는다 — 그 내용은 상위 동반 문서인 cubrid-prior-list.md에 있다. 이 문서는 거기서 개괄한 ### LOG_PRIOR_NODE### LOG_PRIOR_LSA_INFO의 필드 단위 확장판이다. 구조체 선언은 log_record.hpp(온디스크 헤더 + variant 페이로드)와 log_append.hpp(큐 노드, 상태, 헬퍼 입력)에 있으며, 생성자는 log_append.cpp에 있다.

1.1 큐잉 단위 — log_prior_node (LOG_PRIOR_NODE)

섹션 제목: “1.1 큐잉 단위 — log_prior_node (LOG_PRIOR_NODE)”

LOG_PRIOR_NODE는 힙에 할당된 전송 중(in-flight) 로그 레코드 하나다: producer가 빌드했지만 공유 LOG_PAGE 버퍼에 아직 복사되지 않은 상태다. 큐는 이 노드들의 단방향 연결 리스트이며, 각 노드는 최대 세 개의 malloc 페이로드 버퍼를 소유하고, 로그에 바이트 그대로 기록되는 레코드 헤더를 내장한다.

// 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; /* whether the log page which'll contain this node has to be encrypted */
/* data header info */
int data_header_length;
char *data_header;
/* data info */
int ulength;
char *udata;
int rlength;
char *rdata;
LOG_PRIOR_NODE *next;
};
필드역할존재 이유
log_header내장된 LOG_RECORD_HEADER(1.3): trid, type, 세 개의 LSA 링크.노드 자체가 로그 레코드다. Producer가 trid/type을 채우며, LSA 링크는 attach 시 뮤텍스 아래에서 채워진다(Ch 5).
start_lsaattach 시 할당된 prior_lsa의 스냅샷. 주석: “for assertion”.드레인은 복사된 바이트가 이 위치에 정확히 내렸음을 assertion으로 검증한다 — 드리프트 체크용이며, recovery는 읽지 않는다.
tde_encrypted이 노드를 담을 로그 페이지에 TDE 암호화가 필요함을 나타내는 플래그.빌드 시 prior_set_tde_encrypted(Ch 3, Ch 10)에 의해 설정되며, 드레인이 레코드 단위로 페이지에 전파한다.
data_header_lengthdata_header의 바이트 길이.variant 헤더는 타입마다 크기가 다르므로, 길이가 있어야 드레인이 맹목적으로(blindly) 복사할 수 있다.
data_header타입별 헤더(log_rec_* 구조체)를 위한 malloc 버퍼.고정 log_header가변(variant) 헤더를 분리한다(Ch 3–4).
ulengthudata의 바이트 길이. zip/데이터 없음 센티넬로 음수가 될 수 있다(Ch 4).드레인이 복사할 undo 바이트 수를 알려주며, list_size에도 기여한다.
udatamalloc된 undo 버퍼(zlib 압축 가능).롤백/undo를 위한 “before image”; redo 전용 레코드에는 없다.
rlengthrdata의 바이트 길이.redo 바이트 수; ulength와 동일한 이중 역할.
rdatamalloc된 redo 버퍼(압축 또는 diff 인코딩 가능).크래시 redo를 위한 “after image”; undo 전용 레코드에는 없다.
next다음 노드로의 전방 링크, 꼬리에서 NULL.리스트를 단방향 FIFO로 만든다. Attach 시 기록되며(Ch 5), 드레인에 의해 해제된다(Ch 6).

역할 매트릭스 — 레코드 타입별 live 페이로드 세그먼트 (log_rectype 값 기준):

레코드 타입 그룹data_headerudatardata
LOG_UNDOREDO_DATA / LOG_MVCC_UNDOREDO_DATA / LOG_*_DIFF_UNDOREDO_DATAundoredo 헤더undo 이미지redo 이미지 (DIFF 변형에서는 diff 인코딩 — Ch 4)
LOG_UNDO_DATA / LOG_MVCC_UNDO_DATAundo 헤더undo 이미지비어 있음
LOG_REDO_DATA / LOG_MVCC_REDO_DATAredo 헤더비어 있음redo 이미지
LOG_COMMIT / LOG_ABORT 및 기타 제어 레코드소형 고정 헤더(흔히 LOG_REC_DONETIME)비어 있음비어 있음

불변 조건 1.A — 페이로드 포인터와 길이는 일치해야 한다. udata != NULL이면 ulength > 0이 성립해야 하며(대칭적으로 rdata/rlength도 동일), NULL이면 길이는 비양수여야 한다. 포인터가 NULL이 아닌데 길이가 0이면, 드레인이 길이 0의 세그먼트를 기록해 이후 모든 레코드의 offset을 오염시킨다.

불변 조건 1.B — attach 전까지 단독 소유. prior_lsa_next_record가 노드를 스플라이스하기 전(Ch 5)에는 빌더만 포인터를 보유하며, next는 읽어서는 안 되는 쓰레기 값이다. Attach 이후에는 리스트가 소유하며, producer는 더 이상 건드려서는 안 된다. Attach 후 접근하거나 드레인에서 이중 해제하면 경계 넘는 use-after-free가 된다.

Figure 1-1 — LOG_PRIOR_NODE: 고정 헤더, 세 개의 malloc 페이로드 세그먼트, 전방 링크 Figure 1-1 — LOG_PRIOR_NODE: 고정 헤더, 세 개의 malloc 세그먼트, 전방 링크.

1.2 큐 상태 — log_prior_lsa_info (LOG_PRIOR_LSA_INFO)

섹션 제목: “1.2 큐 상태 — log_prior_lsa_info (LOG_PRIOR_LSA_INFO)”

서버 전체에 정확히 하나, 싱글턴 log_Gl.prior_info만 존재한다(스레드별, 볼륨별 복사본 없음). 리스트의 head/tail, LSN 커서, 바이트 회계, 디태치된 “flush” 서브리스트, attach↔detach 뮤텍스를 보유한다.

// log_prior_lsa_info -- src/transaction/log_append.hpp
struct log_prior_lsa_info
{
LOG_LSA prior_lsa;
LOG_LSA prev_lsa;
/* list */
LOG_PRIOR_NODE *prior_list_header;
LOG_PRIOR_NODE *prior_list_tail;
INT64 list_size; /* bytes */
/* flush list */
LOG_PRIOR_NODE *prior_flush_list_header;
std::mutex prior_lsa_mutex;
log_prior_lsa_info ();
};

생성자 log_prior_lsa_info::log_prior_lsa_info(log_append.cpp)는 커서를 NULL_LSA로, 포인터를 NULL로, list_size0으로 초기화한다.

필드변경 주체역할존재 이유
prior_lsaproducer (attach, 뮤텍스 아래)다음에 할당할 LSN; 각 attach는 이 값을 노드에 복사한 뒤 해당 바이트만큼 전진시킨다.단조 증가 LSN 할당기; 뮤텍스 아래에서만 증가하므로 LSN은 gap 없이 엄격히 단조 증가한다.
prev_lsaproducer (attach, 뮤텍스 아래)이전에 attach된 레코드의 주소; log_header.back_lsa에 복사된 뒤 덮어쓰인다.로그 전체의 역방향 체인을 구성한다. 트랜잭션별 prev_tranlsa(1.3)와 구분된다.
prior_list_headerproducer (첫 번째 attach) + drain (detach)드레인되지 않은 노드 FIFO의 head.드레인의 시작점; NULL은 빈 큐를 의미한다.
prior_list_tailproducer (매 attach)FIFO의 tail; 새 노드는 여기에 이어 붙는다.O(1) append.
list_sizeproducer (attach, +=) + drain (detach 시 reset)바이트 합계(sizeof(node) + data_header_length + ulength + rlength).logpb_get_memsize()에 대한 배압(backpressure)(Ch 9).
prior_flush_list_headerdraindetach 이후 리스트의 head; 드레인이 체인을 여기로 재부모화한다.producer가 append하는 대상과 drain이 소비하는 대상을 분리한다.
prior_lsa_mutex양쪽 모두LSN 할당 + attach와 detach를 직렬화하는 std::mutex.다중 producer / 단일 consumer 핸드오프를 안전하게 만든다; splice와 커서 증가 동안만 보유된다(Ch 5).

불변 조건 1.C — 커서는 prior_lsa_mutex 아래에서만 전진한다. prior_lsaprev_lsa는 모두 평범한 LOG_LSA(atomic이 아님)이며, 유일한 기록 경로는 prior_lsa_next_record_internal(Ch 5) 내부의 락 안에 있다. 동시 증가가 발생하면 두 레코드가 겹치는 바이트 범위를 주장하게 되어 로그를 복구 불가 상태로 만든다.

불변 조건 1.D — header/tail 일관성. prior_list_header == NULL이면 prior_list_tail == NULL이고 큐가 비어 있다. 이 관계는 attach 분기의 if (tail == NULL) { header = tail = node; } else { tail->next = node; tail = node; } 패턴(Ch 5)으로 강제된다. 하나만 NULL로 만들면 레코드를 잃거나 낡은 tail에 append하게 된다.

불변 조건 1.E — list_size는 드레인되지 않은 노드를 정확히 추적한다. attach마다 증가하고, drain이 리스트를 detach할 때 0으로 초기화된다. 높게 드리프트하면 배압을 오발동하고, 낮게 드리프트하면 큐가 무한 성장해 로그 영역 메모리 예산을 소진한다.

Figure 1-2 — log_prior_lsa_info: 커서·바이트 회계·세 개의 리스트 포인터를 가진 싱글턴 큐 상태 Figure 1-2 — log_prior_lsa_info: producer가 header/tail FIFO를 늘리고, drain이 그것을 prior_flush_list_header 아래로 재부모화한다.

1.3 내장 레코드 헤더 — log_rec_header (LOG_RECORD_HEADER)

섹션 제목: “1.3 내장 레코드 헤더 — log_rec_header (LOG_RECORD_HEADER)”

log_prior_node.log_headerLOG_RECORD_HEADER다 — 메모리와 디스크 모두에서 모든 로그 레코드의 고정 크기 앞부분(prefix)이며, recovery는 이를 바이트 그대로 읽어 들인다.

// log_rec_header -- src/transaction/log_record.hpp
struct log_rec_header
{
LOG_LSA prev_tranlsa; /* Address of previous log record for the same transaction */
LOG_LSA back_lsa; /* Backward log address */
LOG_LSA forw_lsa; /* Forward log address */
TRANID trid; /* Transaction identifier of the log record */
LOG_RECTYPE type; /* Log record type (e.g., commit, abort) */
};
필드설정 시점역할존재 이유
prev_tranlsaattach(Ch 5), 트랜잭션 디스크립터에서동일 트랜잭션의 이전 레코드로의 역방향 링크.롤백/undo가 트랜잭션 하나를 역순으로 걷게 한다. back_lsa와 구별된다.
back_lsaattach, prior_info.prev_lsa에서로그 전체에서 이전 레코드로의 역방향 링크.전역 역방향 스캔; prev_lsa(1.2)를 소비한다.
forw_lsaattach, 다음 prior_lsa에서다음 레코드로의 전방 링크.redo 패스 중 전방 스캔.
trid빌드(Ch 3)소유 트랜잭션 id.Recovery는 트랜잭션 단위로 그룹화하고, vacuum/CDC는 이 값으로 필터링한다.
type빌드log_rectype 값.Recovery 핸들러를 디스패치하며, 드레인에게 어느 세그먼트가 존재하는지 알려 준다(1.1).

역방향 체인이 두 개 있다: back_lsa(모든 레코드)와 prev_tranlsa(이 트랜잭션만). LOG_LSAlog_lsa.hpp{pageid, offset} 쌍이며, NULL_LSA는 “해당 레코드 없음”을 나타낸다. data_header의 variant 헤더는 통상 LOG_DATA로 시작하며 — 이는 log_data_addr(1.4)가 공급하는 값이다.

1.4 Producer 헬퍼 입력 — log_crumblog_data_addr

섹션 제목: “1.4 Producer 헬퍼 입력 — log_crumb과 log_data_addr”

노드에 저장되지 않으며, 빌드 경로의 입력이다. Ch 3–4에서 이들의 소비자인 prior_lsa_alloc_and_copy_data / prior_lsa_alloc_and_copy_crumbs를 상세히 다룬다.

// log_crumb -- src/transaction/log_append.hpp
struct log_crumb
{
int length;
const void *data;
};
필드역할존재 이유
length이 조각(fragment)의 바이트 길이.”crumb”은 scatter-gather 페이로드의 한 조각이다.
data호출자 소유 바이트에 대한 const 포인터(아직 복사 전).호출자가 undo/redo를 비연속 조각으로 표현할 수 있게 한다; 빌드 경로가 이들을 하나의 버퍼로 이어 붙인다. const는 복사 후 원본을 건드리지 않겠다는 신호다.
// log_data_addr -- src/transaction/log_append.hpp
struct log_data_addr
{
using offset_type = PGLENGTH;
const VFID *vfid; /* File the page belongs to, or NULL when not file-associated */
PAGE_PTR pgptr;
offset_type offset; /* Offset or slot */ // + ctors
};
필드역할존재 이유
vfid수정된 페이지의 파일 id, 또는 파일과 무관한 경우 NULL.data_header(LOG_DATA.volid)에 저장된다; NULL은 합법적인 “파일 없음” 분기다.
pgptr변경 중인 인메모리 페이지를 가리키는 포인터.빌드 경로가 이 포인터로 페이지/볼륨 id를 읽어 LOG_DATA를 채운다.
offset페이지 내 offset(또는 slot).Recovery 대상 offset; 상위 비트는 LOG_RV_RECORD_* 플래그(LOG_RV_RECORD_SET_MODIFY_MODE)로 겸용된다 — Ch 10.

LOG_DATA_ADDR_INITIALIZER{ NULL, NULL, 0 }이다(todo: remove me 표기 있음).

1.5 Append 측 상태 — log_append_info (LOG_APPEND_INFO)

섹션 제목: “1.5 Append 측 상태 — log_append_info (LOG_APPEND_INFO)”

LOG_APPEND_INFO는 반대편의 flush 커서다 — “두 개의 커서”(Ch 2) 중 두 번째이며, Ch 7–8이 읽고 쓰는 내구성(durability) 상태다. prior_lsa(큐)와 nxio_lsa(내구성)를 혼동하지 않도록 주의한다.

필드변경 주체역할존재 이유
vdes로그 초기화 / 아카이빙활성 로그 볼륨의 볼륨 디스크립터(fd) (초기값 NULL_VOLDES).flush 경로가 이 디스크립터로 페이지를 기록한다.
nxio_lsaflush(Ch 8)Atomic. 아직 디스크에 쓰이지 않은 가장 낮은 LSN — WAL 프런티어.커밋 대기자가 락 없이 읽는다; atomic인 것은 이 필드가 뮤텍스 없이 읽히는 유일한 append 측 필드이기 때문이다. get_nxio_lsa/set_nxio_lsa로 접근한다.
prev_lsaappend/copy(Ch 7)페이지에 마지막으로 복사된 레코드의 LSA.큐의 prev_lsa와 대응되는 append 측 미러; 헤더 주석: not really belonging here.
log_pgptrappend/copy현재 고정(fixed)된 권위 있는 LOG_PAGE — 채워지는 중.드레인이 노드 바이트를 복사해 넣는 목적지(Ch 7).
appending_page_tde_encryptedappend/copyappend 중인 페이지에 TDE 암호화가 필요한지 여부.노드 단위 tde_encrypted(1.1)의 페이지 단위 대응물.

WAL 안전 속성은 항상 nxio_lsa <= prior_lsa이다 — 레코드는 flush되기 전에 반드시 큐잉(LSN 할당)되어 있어야 한다. Ch 8에서 atomic load와 뮤텍스 아래 증가가 이 속성을 어떻게 보존하는지 추적한다.

// LOG_PRIOR_LSA_LOCK -- src/transaction/log_append.hpp
enum LOG_PRIOR_LSA_LOCK
{
LOG_PRIOR_LSA_WITHOUT_LOCK = 0,
LOG_PRIOR_LSA_WITH_LOCK = 1
};
의미존재 이유
LOG_PRIOR_LSA_WITHOUT_LOCK (0)호출자가 prior_lsa_mutex보유하지 않은 상태; 헬퍼가 직접 획득해야 한다.일반 경로: prior_lsa_next_record는 락을 보유하지 않은 상태로 진입한다.
LOG_PRIOR_LSA_WITH_LOCK (1)호출자가 이미 prior_lsa_mutex보유 중; 헬퍼는 재획득해서는 안 된다.커밋/postpone 레코드(Ch 5/Ch 10)가 하나의 락 아래서 트랜잭션 상태를 업데이트하면서 동시에 attach해야 할 때 사용. prior_lsa_next_record_with_lock으로 넘긴다.

이것은 prior_lsa_next_record_internal에 관통하는 with_lock 파라미터다. 보유 중에 재획득하거나, 미보유 상태에서 건너뛰는 것은 전형적인 이중 락 / 업데이트 유실 버그다; 이 열거형은 락 상태를 명시적으로 만들어 그 버그를 방지한다.

  1. LOG_PRIOR_NODE는 전송 중인 로그 레코드 하나다: 내장된 LOG_RECORD_HEADER, 세 개의 병렬 (length, ptr) 세그먼트(data_header, udata, rdata), tde_encrypted 플래그, assertion 전용 start_lsa, next 링크. 어느 세그먼트가 live인지는 log_header.type을 따른다(1.1).
  2. log_Gl.prior_info는 유일한 LOG_PRIOR_LSA_INFO 싱글턴으로 큐를 보유한다: FIFO head/tail, prior_lsa/prev_lsa 커서, list_size, 디태치된 prior_flush_list_header, prior_lsa_mutex. Producer는 뮤텍스 아래에서만 FIFO를 늘리고 커서를 전진시키며, drain은 리스트를 detach하고 list_size를 초기화한다.
  3. log_rec_header에는 두 개의 별도 역방향 체인이 있다: back_lsa는 모든 레코드를 연결하고, prev_tranlsa는 동일 트랜잭션의 레코드만 연결한다.
  4. log_crumblog_data_addr는 빌드 시 입력이며 저장된 상태가 아니다 — crumb은 scatter-gather 조각이고, log_data_addr는 대상 파일/페이지/offset을 전달한다(offset 상위 비트 = LOG_RV_RECORD_* 모드).
  5. LOG_APPEND_INFO는 내구성 측 커서다: atomic nxio_lsa는 커밋 대기자가 락 없이 읽는 WAL 프런티어이며, 안전 속성은 nxio_lsa <= prior_lsa다(Ch 8).
  6. LOG_PRIOR_LSA_LOCK은 호출자의 락 상태를 명시적으로 만든다 — attach 헬퍼가 이중 락도, 뮤텍스 건너뜀도 하지 않게 하는 다중 producer attach의 경쟁-자유성의 핵심 장치다.
  7. 이후 챕터들이 보존해야 할 불변 조건: 페이로드/길이 일치(1.A), attach 전 단독 소유(1.B), 뮤텍스 아래에서만 커서 변경(1.C), header/tail 일관성(1.D), list_size 정확한 회계(1.E).

Chapter 2: 초기화 메모리와 두 개의 커서

섹션 제목: “Chapter 2: 초기화 메모리와 두 개의 커서”

Chapter 1은 정적인 구조체 배치를 살펴봤다. 이번 장은 부트 시점의 질문에 답한다: 이 구조체들은 어디서 만들어지고, 노드별 메모리는 어떻게 생성·회수되며, 생산자 쪽 prior_lsa 커서는 페이지 쪽 append_lsa 커서와 어떻게 결합 상태를 유지하는가? 답은 세 곳에 있다 — 큐 상태의 C++ 생성자, log_initialize_internal의 LSN 리셋 매크로, 그리고 스레드별 압축 스크래치 메모리. 이유는 상위 문서(cubrid-prior-list.md §“CUBRID’s Approach”)에 있다.

2.1 큐 상태는 빈 상태로 생성된다

섹션 제목: “2.1 큐 상태는 빈 상태로 생성된다”

log_Gl.prior_info는 전역 log_Gllog_prior_lsa_info 값 멤버다. malloc이나 memset이 아닌, C++ 기본 생성으로 log_Gl과 함께 만들어지며 모든 필드에 의도적인 빈 값이 할당된다.

// log_prior_lsa_info::log_prior_lsa_info -- src/transaction/log_append.cpp
log_prior_lsa_info::log_prior_lsa_info ()
: 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 () { }

각 필드의 생성 초깃값, 부트 시 역할, 존재 이유:

필드역할존재 이유
prior_lsaNULL_LSA; 다음에 부여할 LSA, append 전 재설정됨생산자가 mutex 하에서 전진시키는 커서.
prev_lsaNULL_LSA; 마지막으로 붙인 노드의 LSA, append 전 재설정됨새 노드의 back_lsa(물리 역방향 링크)에 공급됨.
prior_list_headerNULL; 빈 큐 헤드생산자 attach 지점; drain detach 지점.
prior_list_tailNULL; 빈 큐 테일O(1) 꼬리 추가 대상.
list_size0; 큐에 쌓인 바이트 없음배압 트리거(Ch. 9); 회계는 0에서 시작.
prior_flush_list_headerNULL; drain 미실행단일-drain 활성 플래그(Ch. 6); drain CS 외부에서는 NULL.
prior_lsa_mutex기본 std::mutex, 비잠금 상태생산자 경로의 유일한 mutex.

생성자는 런타임 커서 값을 확정하지 않는다prior_lsa / prev_lsa는 여기서 NULL_LSA이며, 유효한 LSN이 아니다. 이 값들은 log_initialize_internal이 시드를 심은 뒤에야(§2.3) 실제 값을 갖는다. 생성은 큐를 일관되고 빈 상태로 만들고, 시드 심기가 활성 상태로 만든다.

2.2 두 리셋 매크로 — 커서 한 쌍을 한 번에 쓰는 단일 작성자

섹션 제목: “2.2 두 리셋 매크로 — 커서 한 쌍을 한 번에 쓰는 단일 작성자”

prior 커서와 페이지 쪽 커서 사이의 결합은 우연히 생긴 것이 아니다. 이 결합은 log_append.cpp의 두 헬퍼(이름은 “매크로”이지만 실제로는 함수)로 생성 시부터 강제된다. 각 헬퍼는 위치를 동시에 쓴다.

// LOG_RESET_APPEND_LSA -- src/transaction/log_append.cpp
LOG_RESET_APPEND_LSA (const LOG_LSA *lsa) {
log_Gl.hdr.append_lsa = *lsa; /* page-side append cursor */
log_Gl.prior_info.prior_lsa = *lsa; } /* producer-side prior cursor -- SAME value */
// LOG_RESET_PREV_LSA -- src/transaction/log_append.cpp
LOG_RESET_PREV_LSA (const LOG_LSA *lsa) {
log_Gl.append.prev_lsa = *lsa; /* page-side prev-record cursor */
log_Gl.prior_info.prev_lsa = *lsa; } /* producer-side prev-record cursor -- SAME value */

핵심은 이것이다: prior_lsa를 설정하면서 hdr.append_lsa를 같은 값으로 설정하지 않는 코드 경로는 없으며, prior_info.prev_lsaappend.prev_lsa도 마찬가지다. 작성자는 항상 이 두 함수 중 하나 — 이것이 커서 결합 불변식(§2.6)의 기계적 근거다. // todo - concurrency safe-guard 주석은 솔직하다: 이 함수들은 부트/복구 중 LOG_CS 하에서 실행되므로 어떤 생산자도 경쟁할 수 없고, 이중 쓰기는 사실상 원자적이며 활성 시스템에서는 호출되지 않는다. 세 번째 헬퍼 LOG_APPEND_PTRlog_pgptr->area + hdr.append_lsa.offset — 페이지 쪽 커서의 바이트 주소를 반환하며, 부트 시 EOF 레코드 복구에 사용된다(아래 Branch C).

2.3 log_initialize_internal에서 커서가 시드되는 위치

섹션 제목: “2.3 log_initialize_internal에서 커서가 시드되는 위치”

log_initialize_internal(log_manager.c)은 부트/재시작 진입점으로, LOG_CS_ENTER 내부에서 실행된다(따라서 이중 쓰기 리셋 매크로는 안전하다). 이 함수는 시작 경로에 따라 세 지점 중 하나에서 두 커서에 시드를 심는다.

Branch A — 미디어 크래시, 마운트 가능한 active log 없음. 헤더를 합성하고 append_lsa{LOGPAGEID_MAX, 0}으로 강제 설정한 뒤, LOG_RESET_APPEND_LSA (&log_Gl.hdr.append_lsa)prior_lsa를 같은 값으로 끌어온다 (인라인 주석: /* sync append_lsa to prior_lsa */).

Branch B — 복구 필요(일반 크래시). 마운트된 active log에서 헤더를 읽어 들이고, log_recovery가 재플레이하면서 같은 매크로로 커서를 재설정하며, 로그의 실제 끝에서 append_lsa를 남긴다.

Branch C — 정상 종료, 복구 불필요. 설명에 가장 적합한 경로로, 페이지와의 결합을 명시적으로 보여 준다. logpb_fetch_start_append_page가 페이지 쪽 커서를 위치시킨 뒤, 페이지에서 EOF 레코드를 읽어 prev_lsaback_lsa로 시드를 심는다.

// log_initialize_internal -- src/transaction/log_manager.c (clean-shutdown branch, condensed)
if (log_Gl.hdr.append_lsa.pageid > 0 || log_Gl.hdr.append_lsa.offset > 0)
{
eof = (LOG_RECORD_HEADER *) LOG_APPEND_PTR (); /* read EOF record at append cursor */
LOG_RESET_PREV_LSA (&eof->back_lsa); /* prev_lsa := eof->back_lsa (both copies) */
}

Branch C에 없는 것에 주목하라: 명시적인 LOG_RESET_APPEND_LSA 호출이 없다. append_lsa는 헤더를 가져올 때(logpb_fetch_header) 설정됐으므로 prior_lsa가 그에 비해 오래된 값을 지닌다 — 이 간극은 §2.4가 닫는다.

2.4 종료 자기 검사 — 불변식을 코드로 구현하기

섹션 제목: “2.4 종료 자기 검사 — 불변식을 코드로 구현하기”

각 branch가 합류한 뒤, log_initialize_internal은 방어적 동등성 검사 한 쌍을 실행한다 — 커서 결합 불변식을 코드로 작성한 것이다.

// log_initialize_internal -- src/transaction/log_manager.c (post-recovery convergence)
if (!LSA_EQ (&log_Gl.append.prev_lsa, &log_Gl.prior_info.prev_lsa))
{ assert (0); LOG_RESET_PREV_LSA (&log_Gl.append.prev_lsa); } /* defense: force equal */
if (!LSA_EQ (&log_Gl.hdr.append_lsa, &log_Gl.prior_info.prior_lsa))
{ assert (0); LOG_RESET_APPEND_LSA (&log_Gl.hdr.append_lsa); } /* defense: force equal */

각 branch를 추적해 보면: 값이 같을 때(기대 결과) 각 검사를 건너뛴다. 불일치가 발생하면 디버그 빌드에서 assert(0)이 발화하고, 릴리스 빌드는 방어 리셋으로 넘어가 prior 쪽 복사본을 페이지 쪽 값으로 강제 동기화한다 — Branch C에서 빠진 LOG_RESET_APPEND_LSA를 이 방식으로 보정한다. 두 검사를 통과(또는 보정)한 뒤, 생산자는 prior_lsa == append_lsa, prior_info.prev_lsa == append.prev_lsa 상태로 동작을 시작한다. 데이터베이스 생성 경로는 보다 직접적으로 동일한 상태에 도달한다 — LSA_SET_NULLLOG_RESET_PREV_LSA(&log_Gl.append.prev_lsa)를 실행해 두 prev_lsa 복사본을 모두 NULL_LSA로 동기화한다.

2.5 노드별 메모리 — 생산자가 malloc, drain이 free

섹션 제목: “2.5 노드별 메모리 — 생산자가 malloc, drain이 free”

노드 풀은 없다. LOG_PRIOR_NODE는 파이프라인의 양 끝에서 할당·회수되는 자유 저장소 객체다.

  • 생산자가 할당prior_lsa_alloc_and_copy_data(및 파생인 _crumbs)는 prior mutex 외부에서 단일 노드 malloc을 수행한 뒤 길이를 0으로, 포인터를 NULL로 초기화한다.
// prior_lsa_alloc_and_copy_data -- src/transaction/log_append.cpp (condensed)
node = (LOG_PRIOR_NODE *) malloc (sizeof (LOG_PRIOR_NODE));
if (node == NULL) { er_set (...ER_OUT_OF_VIRTUAL_MEMORY...); return NULL; }
node->log_header.type = rec_type; node->tde_encrypted = false;
/* data_header / udata / rdata lengths set 0, all four pointers + next set NULL */

각 타입별 생성자는 이후 data_header / udata / rdata를 별도로 malloc한다 (Ch. 3-4) — 레코드당 최대 네 번의 힙 할당.

  • drain이 해제 — 분리된 노드와 세 개의 바이트 버퍼는 drain 순회 (logpb_append_prior_lsa_list, Ch. 6)에서 바이트가 로그 페이지에 기록된 후 해제된다.

불변식 — 노드 소유권은 단일하며 attach 시점에 이전된다. attach 전에는 생산자가 노드를 소유하고 오류 경로에서 해제할 수 있다. prior_lsa_next_record가 mutex 하에서 노드를 꼬리에 연결한 이후에는 소유권이 drain에게 넘어가며, 생산자는 더 이상 해당 노드를 건드려서는 안 된다. 이는 구조적으로 강제된다: attach 함수는 할당된 LOG_LSA만 반환하고 노드 포인터는 반환하지 않으므로, 올바른 호출자는 오용할 핸들을 갖지 않는다 — 캐시된 포인터는 drain이 해제한 순간 use-after-free가 된다.

list_size 회계는 연결 시점에 시작된다: attach는 sizeof(LOG_PRIOR_NODE) + data_header_length + ulength + rlengthlog_Gl.prior_info.list_size에 더한다 — 생성 시 0에서 시작해 첫 attach까지 0을 유지하며, drain이 detach 시점에 초기화한다(Ch. 6).

불변식 — prior 커서와 페이지 쪽 커서는 초기화 시 동기화된 상태에서 시작하며 그 상태를 유지한다. 초기화 완료 시점:

log_Gl.prior_info.prior_lsa == log_Gl.hdr.append_lsa
log_Gl.prior_info.prev_lsa == log_Gl.append.prev_lsa

확립 방법. 부트 시에는 LOG_RESET_APPEND_LSA / LOG_RESET_PREV_LSA만이 이 필드를 작성하며, 각각 두 복사본을 같은 값으로 설정한다(§2.2). §2.4의 자기 검사는 어떤 편차도 페이지 쪽으로 보정한다.

런타임에 유지되는 방법. 이후 커서들은 설계된 범위 내에서 분기한다: 생산자는 prior_lsa_mutex 하에서 prior_lsa를 전진시키는 반면(Ch. 5), append_lsa는 drain이 레코드를 페이지에 복사할 때까지 뒤처진다(Ch. 7). 따라서 append_lsa <= prior_lsa이고, 간극은 큐에 쌓인 바이트다. logpb_append_next_record는 drain되는 모든 노드를 if (!LSA_EQ (&node->start_lsa, &log_Gl.hdr.append_lsa)) logpb_fatal_error (...)로 검사하므로, 페이지 커서는 prior 커서가 부여한 정확한 LSA를 따라 진행한다.

위반 시 발생하는 문제. append_lsa보다 뒤에서 시작하면 생산자가 이미 소비된 LSA를 재발급하게 되어 중복 LSN, 즉 손상된 로그가 발생한다. 앞에서 시작하면 첫 번째로 drain되는 노드의 start_lsa가 위의 가드 검사에 실패해 logpb_fatal_error를 유발한다. 부트 시 동등성은 단일 LSA 작성자 방식의 전제 조건이다.

2.7 압축 스크래치 — 큐 바깥에 따로

섹션 제목: “2.7 압축 스크래치 — 큐 바깥에 따로”

압축 스테이징 메모리는 큐의 일부가 아니며 노드별도 아니다. 이것은 스레드별 스크래치로, 생산자가 노드의 최종 버퍼를 할당하기 전에 페이로드를 zlib으로 압축할 수 있게 한다. 두 가지 구성 요소와 생명주기 훅으로 이루어진다.

(1) zlib 컨텍스트 (LOG_ZIP), 워커 스레드당 지연 할당 — SERVER_MODE에서는 THREAD_ENTRY에, SA에서는 파일 정적 변수로 존재한다. SERVER_MODE에서 log_append_get_zip_undo(그리고 동일한 구조의 _redo)는 null인 thread_pthread_get_thread_entry_info()로 해소하고, 여전히 null이면 NULL을 반환하며, 그렇지 않으면 첫 사용 시 log_zip_alloc (IO_PAGESIZE)으로 할당하고 반환한다. SA의 #else 분기는 파일 정적 log_zip_undo를 반환한다. 이 null 스레드 가드, 첫 사용 시 지연 할당, OOM 시 NULL/false 반환 패턴은 get_zip_redo, get_data_ptr, realloc_data_ptr이 공유한다. THREAD_ENTRY 필드는 NULL로 생성되고 ~thread_entry에서 log_zip_free로 해제되므로, 각 워커는 자신만의 압축기를 소유하며 경쟁이 없다.

(2) 생명주기 훅. log_append_init_zip(logpb_initialize_pool에서 LOG_CS 하에 호출)은 PRM_ID_LOG_COMPRESS가 꺼져 있으면 log_Zip_support = false로 설정한다. 켜져 있으면 SERVER_MODE에서는 true만 설정하고(컨텍스트는 get_zip_undo/_redo에서 지연 할당), SA의 #else 분기는 log_zip_undo/redolog_data_ptr = malloc (IO_PAGESIZE * 2)를 즉시 할당하며, 실패 시 성공한 것을 해제하고 log_Zip_support를 false로 초기화한다. log_append_final_zipif (!log_Zip_support) return;으로 일찍 반환한다. SERVER_MODE 분기는 비어 있으며 (스레드 소멸자가 정리), SA의 #else 분기는 두 컨텍스트와 log_data_ptr을 모두 해제한다.

(3) 스테이징 버퍼 log_data_ptr — 스레드별(SA에서는 전역) 스크래치로, log_zip에 공급할 때까지 연결된, 비압축 crumb들을 보관한다. 두 접근자는 (1)과 같은 패턴을 공유한다.

// log_append_get_data_ptr -- src/transaction/log_append.cpp (SERVER_MODE, condensed)
if (thread_p == NULL) thread_p = thread_get_thread_entry_info ();
if (thread_p == NULL) return NULL; /* null-thread early-return */
if (thread_p->log_data_ptr == NULL) { /* first use this thread */
thread_p->log_data_length = IO_PAGESIZE * 2;
thread_p->log_data_ptr = malloc (thread_p->log_data_length);
if (thread_p->log_data_ptr == NULL) /* OOM */
{ thread_p->log_data_length = 0; er_set (...ER_OUT_OF_VIRTUAL_MEMORY...); } }
return thread_p->log_data_ptr; /* may be NULL on OOM */

get_data_ptr(위)은 첫 사용 시 IO_PAGESIZE * 2를 지연 할당한다. log_append_realloc_data_ptr은 확장 분기를 추가한다: log_data_length < length일 때만 확장하며, alloc_lenCEIL_PTVDIVIO_PAGESIZE 단위로 올림한 뒤 realloc한다. realloc OOM 시에는 기존 버퍼를 해제하고 길이를 0으로 초기화한 뒤 false를 반환한다. SA의 #else 분기 두 곳은 파일 정적 log_data_ptr / log_data_length에도 동일한 로직을 수행한다.

유일한 소비자는 undoredo 생성자(prior_lsa_gen_undoredo_record_from_crumbs, Ch. 4)다. 이 영역의 어느 것도 log_Gl.prior_infoprior_lsa_mutex를 건드리지 않으므로, mutex 외부에서 “빌드” 작업을 저렴하게 수행할 수 있다.

flowchart LR
  subgraph THR["THREAD_ENTRY 당 (SERVER_MODE)"]
    ZU["log_zip_undo\n지연 할당 LOG_ZIP"]
    ZR["log_zip_redo\n지연 할당 LOG_ZIP"]
    SB["log_data_ptr\n페이지 단위 정렬 스크래치"]
  end
  CR["crumbs 입력"] --> SB
  SB -->|log_zip| ZU
  SB -->|log_zip| ZR
  ZU --> UD["node->udata\nmalloc, 압축됨"]
  ZR --> RD["node->rdata\nmalloc, 압축됨"]

Figure 2-1 — 압축 스테이징은 스레드별로 분리되어 큐와 독립적이다: crumb들은 log_data_ptr에 연결되고, 스레드별 zlib 컨텍스트로 압축되어 노드 자신의 udata/rdata로 출력된다. prior 큐와 그 mutex는 건드리지 않는다.

  1. 큐 상태는 C++ 생성자로 빈 상태로 만들어진다, 메모리 제로화가 아니다 — 모든 필드가 NULL_LSA / NULL / 0으로, 일관되지만 커서는 아직 사용 불가.

  2. 두 리셋 헬퍼가 각각 두 커서를 쓴다LOG_RESET_APPEND_LSAhdr.append_lsaprior_info.prior_lsa를 쌍으로 묶고, LOG_RESET_PREV_LSA는 두 prev_lsa 복사본을 쌍으로 묶는다. 부트 시 유일한 작성자이므로 이 함수들이 구조적으로 커서를 결합한다.

  3. log_initialize_internal은 시드를 심은 뒤 자기 검사를 수행한다 — 세 가지 시작 branch에 걸쳐 페이지 쪽 헤더에서 시드를 심고, 이어서 LSA_EQ 쌍(assert(0) + 페이지 쪽으로 방어 리셋)으로 두 복사본이 일치함을 보장한다.

  4. 커서 결합 불변식 — 커서는 동기화된 상태에서 시작해 경계 내에서 유지된다 (append_lsa <= prior_lsa, 간극 = 큐에 쌓인 바이트). 뒤에서 시작하면 LSN이 중복되고, 앞에서 시작하면 logpb_append_next_recordstart_lsa == append_lsa 가드가 logpb_fatal_error를 유발한다.

  5. 노드는 생산자가 malloc하고 drain이 free한다 — 풀 없음. 소유권은 attach 시점에 이전되며, API는 LSA만 반환하므로 생산자는 핸들을 갖지 않는다. list_size는 0에서 시작해 attach 시점에 증가한다.

  6. 압축 스크래치는 스레드별이며 큐와 독립적이다THREAD_ENTRY당 지연 할당되는 LOG_ZIP 컨텍스트와 페이지 단위 정렬의 log_data_ptr로 생산자가 prior_lsa_mutex 외부에서 압축할 수 있다. init_zip은 서버 모드에서 log_Zip_support만 설정하고 (즉시 할당은 SA); final_zip은 SA에서만 해제한다.

Chapter 3: Non-Undoredo 레코드를 위해 Mutex 바깥에서 노드 구성하기

섹션 제목: “Chapter 3: Non-Undoredo 레코드를 위해 Mutex 바깥에서 노드 구성하기”

이 챕터가 답하는 독자 질문: 트랜잭션은 어떻게 append-API 호출 하나를 전역 상태에 손대지 않고 완전히 구성된 LOG_PRIOR_NODE로 만드는가? 챕터 2에서 두 커서와 그것을 보호하는 mutex를 살펴보았다. 이 챕터는 그 mutex에 진입하기 이전 단계에 집중한다. 모든 작업은 호출 워커 스레드의 private 스택 위에서 실행되며 — private 메모리, private 페이로드 — 완성된 노드는 log_Gl.prior_info에 한 번도 노출되지 않는다. 노드 attach(챕터 5)와 drain(챕터 6)은 이후에 다룬다.

진입점은 prior_lsa_alloc_and_copy_data다. 자매 함수 prior_lsa_alloc_and_copy_crumbs(챕터 4)는 압축/diff 인코딩을 갖는 undoredo 계열을 처리하고, 이 챕터는 non-undoredo 경로를 다룬다 — postpone, dbout-redo, 2PC prepare, end-checkpoint, 그리고 나머지 소형 레코드 타입 전체를 아우르는 catch-all.

각 생성자(generator)는 새로 mallocLOG_PRIOR_NODE를 전달받아 여기에 매달린 세 개의 독립 할당 버퍼를 채운다 — data header, undo 페이로드, redo 페이로드.

// 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온디스크 LOG_RECORD_HEADER; 여기서는 .type만 설정되고, LSA 링크와 .trid는 mutex 아래(챕터 5)에서 채워진다.copy-out(챕터 7)이 그대로 내보낸다.
start_lsaattach LSA의 assertion 전용 사본; 여기서는 사용하지 않는다.노드가 약속된 위치에 착지했는지 단언한다.
tde_encryptedTDE 암호화 플래그; 여기서는 false.non-undoredo에서는 분류를 미룬다.
data_header_lengthdata_header의 바이트 크기.copy-out이 정확히 이 크기만큼 내보낸다.
data_header타입별 헤더를 위한 힙 버퍼.레코드 타입에 상관없이 노드를 범용으로 유지한다.
ulength / udataundo 페이로드의 길이 + 힙 버퍼.2PC 전역 트랜잭션 정보, checkpoint 트랜잭션 배열.
rlength / rdataredo 페이로드의 길이 + 힙 버퍼.postpone 복구 데이터, dbout-redo 이미지.
nextattach 후 리스트 링크.구성 중에는 NULL; attach(챕터 5)에서 연결된다.

불변 조건 — 노드는 자신이 할당한 버퍼만 소유하며 그 외에는 아무것도 갖지 않는다. 구성 과정에서 호출자 포인터를 aliasing하는 일은 절대 없다: udata/rdata/data_header는 항상 새로 malloc된 후 memcpy된 내용이다(또는 NULL). 덕분에 호출자는 함수가 반환하는 즉시 자신의 scratch 메모리를 해제할 수 있고, 오류 경로(3.7)는 세 포인터 모두를 이중 해제나 borrowed 메모리 해제 걱정 없이 free_and_init으로 처리할 수 있다.

3.2 prior_lsa_alloc_and_copy_data: 할당, 초기화, 분기

섹션 제목: “3.2 prior_lsa_alloc_and_copy_data: 할당, 초기화, 분기”

이 함수는 노드를 malloc하고 모든 필드를 직접 영으로 초기화한다(malloc이지 calloc이 아니므로 세 버퍼 포인터는 초기값이 쓰레기값임), 그다음 rec_type으로 switch한다.

// prior_lsa_alloc_and_copy_data -- src/transaction/log_append.cpp
node = (LOG_PRIOR_NODE *) malloc (sizeof (LOG_PRIOR_NODE));
if (node == NULL) { er_set (... ER_OUT_OF_VIRTUAL_MEMORY ...); return NULL; } /* <- 아직 정리할 것 없음 */
node->log_header.type = rec_type; /* <- mutex 진입 전에 설정되는 유일한 헤더 필드 */
node->data_header_length = 0; node->data_header = NULL;
node->tde_encrypted = false;
node->ulength = 0; node->udata = NULL;
node->rlength = 0; node->rdata = NULL;
node->next = NULL;

포인터를 NULL로 설정한 뒤에 생성자를 실행하기 때문에 통합 오류 경로(3.7)가 안전해진다: 실패한 생성자가 할당하지 못한 버퍼는 반드시 NULL이다.

flowchart TD
  A["rec_type으로 진입"] --> M["노드 malloc; NULL이면 NULL 반환"]
  M --> Z["header.type, tde, length, 포인터, next 초기화"]
  Z --> SW{"switch (rec_type)"}
  SW -->|"UNDOREDO / UNDO / REDO / MVCC_* 계열"| BAD["assert_release(false)<br/>error = ER_FAILED<br/>_crumbs 경로 사용 필수"]
  SW -->|LOG_DBEXTERN_REDO_DATA| G1["prior_lsa_gen_dbout_redo_record"]
  SW -->|LOG_POSTPONE| G2["ulength==0, udata==NULL 단언<br/>prior_lsa_gen_postpone_record"]
  SW -->|LOG_2PC_PREPARE| G3["addr==NULL 단언<br/>prior_lsa_gen_2pc_prepare_record"]
  SW -->|LOG_END_CHKPT| G4["addr==NULL 단언<br/>prior_lsa_gen_end_chkpt_record"]
  SW -->|"~25개 소형 타입: COMMIT, ABORT, SAVEPOINT, RUN_POSTPONE, ..."| G5["rlength==0, rdata==NULL 단언<br/>prior_lsa_gen_record"]
  SW -->|default| D["fall through, error는 NO_ERROR 유지"]
  BAD --> CK{"error_code == NO_ERROR?"}
  G1 --> CK
  G2 --> CK
  G3 --> CK
  G4 --> CK
  G5 --> CK
  D --> CK
  CK -->|yes| RET["node 반환"]
  CK -->|no| FREE["data_header, udata, rdata, node 해제<br/>NULL 반환"]

Figure 3-1. prior_lsa_alloc_and_copy_data의 분기 완전 제어 흐름.

핵심 분기 사실: undoredo/MVCC 계열은 여기서 하드 오류다 — 해당 9개 rec_typeassert_release (false)에 도달해 error_code = ER_FAILED를 설정한다. 압축/diff 기계는 오직 crumbs 경로(챕터 4)에만 있기 때문이다. 사전 dispatch assert들은 각 나머지 계열의 API 계약을 인코딩한다. default 분기는 버퍼가 모두 NULL인 노드를 반환한다 — 헤더만 존재하는 레코드 타입을 위한 경로다.

3.3 prior_lsa_gen_postpone_record: redo 전용 페이지 주소 레코드

섹션 제목: “3.3 prior_lsa_gen_postpone_record: redo 전용 페이지 주소 레코드”

LOG_POSTPONE는 commit으로 미뤄진 연산의 주소와 복구 데이터를 기록한다. 데이터 헤더는 LOG_REC_REDO다: 복구 위치를 담는 LOG_DATA와 redo length로 구성되며, redo 페이로드는 복구 데이터다.

// log_rec_redo -- src/transaction/log_record.hpp
struct log_rec_redo {
LOG_DATA data; /* Location of recovery data */
int length; /* Length of redo data */
};
필드역할존재 이유
data (LOG_DATA)복구 위치 rcvindex + (volid, pageid, offset), 생성자가 모두 기록한다.복구 코드가 rcvindex로 dispatch하고 페이지 slot에 재적용한다.
length (int)redo 이미지의 바이트 길이.node->rlength에 그대로 반영된다.
// prior_lsa_gen_postpone_record -- src/transaction/log_append.cpp
node->data_header_length = sizeof (LOG_REC_REDO);
node->data_header = (char *) malloc (node->data_header_length);
if (node->data_header == NULL) { ...; return ER_OUT_OF_VIRTUAL_MEMORY; }
redo = (LOG_REC_REDO *) node->data_header;
redo->data.rcvindex = rcvindex;
if (addr->pgptr != NULL) /* <- 페이지 상주: VPID 읽기 */
{ vpid = pgbuf_get_vpid_ptr (addr->pgptr);
redo->data.pageid = vpid->pageid; redo->data.volid = vpid->volid; }
else /* <- 페이지 없음: null 위치 */
{ redo->data.pageid = NULL_PAGEID; redo->data.volid = NULL_VOLID; }
redo->data.offset = addr->offset;
redo->length = length;
error_code = prior_lsa_copy_redo_data_to_node (node, redo->length, data);
return error_code;

유일한 분기는 addr->pgptr: 페이지가 있으면 해당 페이지의 (volid, pageid)를 읽고, 없으면 위치를 null로 설정하고 offset만 남긴다. 어느 경우든 redo 데이터는 3.6의 헬퍼로 복사된다.

3.4 prior_lsa_gen_dbout_redo_record: 외부(비페이지) 목적지로의 redo

섹션 제목: “3.4 prior_lsa_gen_dbout_redo_record: 외부(비페이지) 목적지로의 redo”

LOG_DBEXTERN_REDO_DATA는 page buffer 바깥의 대상(예: 파일 수준 연산)을 향한 redo에 사용된다. 헤더는 간결한 LOG_REC_DBOUT_REDO — 복구 인덱스와 길이, 페이지 주소 없음.

// log_rec_dbout_redo -- src/transaction/log_record.hpp
struct log_rec_dbout_redo {
LOG_RCVINDEX rcvindex; /* Index to recovery function */
int length; /* Length of redo data */
};
필드역할존재 이유
rcvindex (LOG_RCVINDEX)외부 redo를 재실행하는 복구 함수 테이블의 인덱스.페이지 주소 없이 인덱스만으로 대상을 식별한다.
length (int)redo 이미지의 바이트 길이.node->rlength에 반영된다.
// prior_lsa_gen_dbout_redo_record -- src/transaction/log_append.cpp
node->data_header_length = sizeof (LOG_REC_DBOUT_REDO);
node->data_header = (char *) malloc (node->data_header_length);
if (node->data_header == NULL) { ...; return ER_OUT_OF_VIRTUAL_MEMORY; }
dbout_redo = (LOG_REC_DBOUT_REDO *) node->data_header;
dbout_redo->rcvindex = rcvindex;
dbout_redo->length = length;
error_code = prior_lsa_copy_redo_data_to_node (node, dbout_redo->length, data);
return error_code;

malloc 실패 체크 이외에 분기가 없다: 헤더 복사, redo 복사, 끝.

3.5 prior_lsa_gen_2pc_prepare_record와 prior_lsa_gen_end_chkpt_record: 이중 페이로드 헤더

섹션 제목: “3.5 prior_lsa_gen_2pc_prepare_record와 prior_lsa_gen_end_chkpt_record: 이중 페이로드 헤더”

이 두 함수는 구조가 같다: 고정 헤더 struct를 할당하고, 조건에 따라 undo 측과 redo 측 버퍼를 복사한다. 헤더 필드를 직접 채우지는 않는다 — 호출자가 반환 후 채운다. 생성자는 두 복사만 수행한다.

// prior_lsa_gen_2pc_prepare_record -- src/transaction/log_append.cpp
node->data_header_length = sizeof (LOG_REC_2PC_PREPCOMMIT);
node->data_header = (char *) malloc (node->data_header_length); /* NULL이면 -> ER_OUT_OF_VIRTUAL_MEMORY 반환 */
if (gtran_length > 0) /* <- 전역 트랜잭션 정보 -> undo 측 */
error_code = prior_lsa_copy_undo_data_to_node (node, gtran_length, gtran_data);
if (lock_length > 0) /* <- 획득한 lock 목록 -> redo 측 */
error_code = prior_lsa_copy_redo_data_to_node (node, lock_length, lock_data);
return error_code;
// prior_lsa_gen_end_chkpt_record -- src/transaction/log_append.cpp
node->data_header_length = sizeof (LOG_REC_CHKPT);
node->data_header = (char *) malloc (node->data_header_length); /* NULL이면 -> ER_OUT_OF_VIRTUAL_MEMORY 반환 */
if (tran_length > 0) /* <- 활성 트랜잭션 디스크립터 -> undo 측 */
error_code = prior_lsa_copy_undo_data_to_node (node, tran_length, tran_data);
if (topop_length > 0) /* <- 시스템 op 디스크립터 -> redo 측 */
error_code = prior_lsa_copy_redo_data_to_node (node, topop_length, topop_data);
return error_code;

분기 주의 사항 — error_code는 누적되지 않고 덮어써진다. redo 측 복사가 error_code를 무조건 대입하므로, redo 측이 성공하면 undo 측 복사 실패가 가려진다. 실제로는 udata 할당에서 OOM이 발생하면 그 직후 rdata malloc도 거의 반드시 실패하고, 헬퍼는 빈 length에 no-op으로 동작하므로 오류가 살아남는다. 세 번째 페이로드를 추가하는 수정자는 “마지막 복사가 이긴다” 동작을 유지하거나 short-circuit으로 전환해야 한다.

헤더 struct의 필드는 이 챕터 바깥에서 채워지지만, 레코드 계약의 일부로서 각 역할을 여기서 정리한다:

// log_rec_2pc_prepcommit -- src/transaction/log_record.hpp
struct log_rec_2pc_prepcommit {
char user_name[DB_MAX_USER_LENGTH + 1];
int gtrid; int gtrinfo_length;
unsigned int num_object_locks; unsigned int num_page_locks;
};
필드역할존재 이유
user_name전역 트랜잭션의 클라이언트 이름.재시작 후 2PC 재구성 시 참여자를 식별한다.
gtrid전역 트랜잭션 식별자.이 로컬 브랜치를 분산 트랜잭션에 연결한다.
gtrinfo_lengthudata에 복사된 전역 트랜잭션 정보 blob의 바이트 길이.undo 페이로드 중 전역 트랜잭션 정보의 크기.
num_object_locks보유한 업데이트 타입 객체 lock 수.복구 시 정확히 그 객체 lock들을 재획득한다.
num_page_locks보유한 업데이트 타입 페이지 lock 수.페이지 lock도 마찬가지; 목록은 redo 페이로드에 실린다.
// log_rec_chkpt -- src/transaction/log_record.hpp
struct log_rec_chkpt {
LOG_LSA redo_lsa; /* Oldest LSA of dirty data page in page buffers */
int ntrans; /* Number of active transactions */
int ntops; /* Total number of system operations */
};
필드역할존재 이유
redo_lsacheckpoint 시점에 dirty 데이터 페이지 중 가장 오래된 LSA.재시작 복구가 redo 패스를 여기서 시작한다.
ntrans캡처된 활성 트랜잭션 수.undo 페이로드 내 LOG_INFO_CHKPT_TRANS의 개수.
ntops캡처된 진행 중인 시스템 op 총 수.redo 페이로드 내 LOG_INFO_CHKPT_SYSOP의 개수.

LOG_END_CHKPT는 두 가지 경로로 도달할 수 있다 — 위의 전용 생성자(3.2의 dispatch가 라우팅)와, 파라미터 없는 변형을 위해 LOG_REC_CHKPT 크기만 결정하는 prior_lsa_gen_recordcase LOG_END_CHKPT 분기.

3.6 prior_lsa_gen_record: catch-all과 복사 헬퍼

섹션 제목: “3.6 prior_lsa_gen_record: catch-all과 복사 헬퍼”

prior_lsa_gen_record는 약 24개의 소형 레코드 타입을 처리한다: data_header_length만 결정하는 switch, 공유 malloc-and-zero 블록, 그리고 하나의 선택적 undo 복사.

// prior_lsa_gen_record -- src/transaction/log_append.cpp
node->data_header_length = 0;
switch (rec_type)
{
case LOG_DUMMY_HEAD_POSTPONE: ... case LOG_SYSOP_ATOMIC_START:
assert (length == 0 && data == NULL); /* <- 순수 마커 레코드: 헤더 없음 */
break;
case LOG_RUN_POSTPONE: node->data_header_length = sizeof (LOG_REC_RUN_POSTPONE); break;
case LOG_COMPENSATE: node->data_header_length = sizeof (LOG_REC_COMPENSATE); break;
case LOG_SAVEPOINT: node->data_header_length = sizeof (LOG_REC_SAVEPT); break;
case LOG_COMMIT:
case LOG_ABORT:
assert (length == 0 && data == NULL);
node->data_header_length = sizeof (LOG_REC_DONETIME); break; /* <- 종료 시각 레코드 */
case LOG_SYSOP_END: node->data_header_length = sizeof (LOG_REC_SYSOP_END); break;
case LOG_2PC_START: node->data_header_length = sizeof (LOG_REC_2PC_START); break;
case LOG_END_CHKPT: node->data_header_length = sizeof (LOG_REC_CHKPT); break;
/* ... 축약: COMMIT_WITH_POSTPONE[_OBSOLETE], SYSOP_START_POSTPONE,
DUMMY_HA_SERVER_STATE, REPLICATION_{DATA,STATEMENT}, SUPPLEMENTAL_INFO ... */
default: break; /* <- length 0 유지: 헤더 없는 레코드 */
}
if (node->data_header_length > 0)
{
node->data_header = (char *) malloc (node->data_header_length);
if (node->data_header == NULL) { ...; return ER_OUT_OF_VIRTUAL_MEMORY; }
#if !defined (NDEBUG)
memset (node->data_header, 0, node->data_header_length); /* <- valgrind 경고 방지 */
#endif
}
if (length > 0)
error_code = prior_lsa_copy_undo_data_to_node (node, length, data); /* <- redo 측은 비어 있음 */
return error_code;

핵심 두 가지. switch는 헤더 크기만 결정할 뿐 필드를 채우지 않는다: 타입별 필드(예: LOG_REC_DONETIME.at_time, LOG_REC_SYSOP_END union)는 호출자가 반환 후 node->data_header를 캐스팅해서 기록한다. 그리고 gen_record는 undo 전용이다: prior_lsa_copy_undo_data_to_node만 호출하며, 이는 dispatcher의 assert (rlength == 0 && rdata == NULL) 가드와 일치한다. 마커 assert는 페이로드 없는 타입에 페이로드를 거부한다. release 빌드에서는 assert를 건너뛰지만 if (length > 0)에는 여전히 도달하므로, 복구 입장에서는 잘못된 페이로드가 복사될 수 있다.

이 그룹에서 가장 작은 의미 있는 헤더는 commit/abort 종료 레코드다:

// log_rec_donetime -- src/transaction/log_record.hpp
struct log_rec_donetime {
INT64 at_time; /* Database creation time. For safety reasons */
};
필드역할존재 이유
at_time트랜잭션이 종료된(commit 또는 abort) 벽시계 시각.시점 복구(point-in-time recovery); 재시작 시 DB 생성 시각과의 정합성 확인.

두 페이로드 복사 헬퍼는 어느 노드 필드를 건드리냐는 점을 제외하면 동일하다:

// prior_lsa_copy_undo_data_to_node -- src/transaction/log_append.cpp
static int prior_lsa_copy_undo_data_to_node (LOG_PRIOR_NODE *node, int length, const char *data)
{
if (length <= 0 || data == NULL) return NO_ERROR; /* <- no-op 가드: 빈 입력에 멱등 */
node->udata = (char *) malloc (length);
if (node->udata == NULL) { ...; return ER_OUT_OF_VIRTUAL_MEMORY; }
memcpy (node->udata, data, length);
node->ulength = length; /* <- malloc+memcpy 성공 후에만 length 설정 */
return NO_ERROR;
}

prior_lsa_copy_redo_data_to_noderdata/rlength로 치환된 것 외에 바이트 단위로 동일하다. 공유 no-op 가드 덕분에 3.5의 호출자는 if (gtran_length > 0) 가드를 쓰고 3.3/3.4는 헬퍼를 무조건 호출해도 모두 안전하다.

불변 조건 — ulength/rlength는 대응 버퍼가 할당된 경우에만 0이 아니다. length는 malloc+memcpy 성공 후에만 설정되고, no-op 가드는 두 값을 초기화된 기본값인 0으로 남긴다. 이 경로에서 ulength > 0이면서 udata == NULL(챕터 7 copy-out에서 크래시) 또는 udata != NULL이면서 ulength == 0(조용한 메모리 누수)인 상태는 발생할 수 없다. length를 복사 성공 전에 설정하는 수정은 이 불변 조건을 깨뜨린다.

3.7 통합 오류 경로와 no-mutex 횡단 불변 조건

섹션 제목: “3.7 통합 오류 경로와 no-mutex 횡단 불변 조건”

prior_lsa_alloc_and_copy_data로 돌아와서, 모든 생성자는 int를 공유 error_code에 반환한다. 끝부분은 단 하나의 판단이다:

// prior_lsa_alloc_and_copy_data (tail) -- src/transaction/log_append.cpp
if (error_code == NO_ERROR)
return node;
else
{
if (node != NULL)
{
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 NULL;
}

이것이 올바르게 동작하는 이유는 정확히 3.2의 직접 초기화와 3.6의 iff 불변 조건 덕분이다: 생성자가 할당한 버퍼는 non-NULL이어서 해제되고, 도달하지 못한 버퍼는 NULL이어서 건너뛴다. 각 포인터의 NULL 여부 자체가 진행 상황 장부 역할을 한다 — 별도의 progress 플래그는 없다.

불변 조건(이 챕터의 핵심) — 이 경로의 어느 곳도 prior_lsa_mutex를 잠그거나 log_Gl.prior_info를 읽거나 쓰지 않는다. 코드로 직접 확인할 수 있다: prior_lsa_alloc_and_copy_data와 다섯 생성자 모두 로컬 node, 호출자의 인자, pgbuf_get_vpid_ptr(page buffer 읽기)만 참조한다. 이로써 노드 구성은 워커 스레드 간에 완전히 병렬로 수행되며, 직렬화 지점(mutex)은 챕터 5의 attach 시점으로 미뤄진다. 이 경로에서 log_Gl.prior_info를 들여다보는 수정이 생기면 hot-path 경합이 재도입된다 — mutex 없이는 데이터 레이스다.

공개 append-API(log_append_undoredo_data, log_append_redo_data, log_append_postpone, checkpoint 및 2PC 로거)는 모두 이 함수 또는 prior_lsa_alloc_and_copy_crumbs로 수렴된다. 전체 목록은 상위 수준 companion 문서 — cubrid-prior-list.md의 append-API 절을 참조한다.

  1. prior_lsa_alloc_and_copy_data는 호출 스레드 위에서 LOG_PRIOR_NODE를 구성한다: malloc 하나, 모든 필드의 직접 초기화(malloc이므로 calloc이 아님), rec_type에 따른 타입별 생성자 dispatch, 그리고 단 하나의 정리 판단.
  2. undoredo/MVCC 계열은 여기서 하드 assert_release (false)다. 이 레코드들은 이 경로에 없는 압축/diff 인코딩을 갖춘 prior_lsa_alloc_and_copy_crumbs(챕터 4)를 거쳐야 한다.
  3. 각 생성자는 타입별 헤더(LOG_REC_REDO postpone, LOG_REC_DBOUT_REDO external redo, LOG_REC_2PC_PREPCOMMIT prepare, LOG_REC_CHKPT end-checkpoint, prior_lsa_gen_record의 ~15가지 크기 중 하나)를 malloc하고, 최대 하나의 undo와 하나의 redo 페이로드를 복사한다.
  4. 생성자 내부의 실질적인 분기는 postpone 하나뿐이다: 페이지 상주 addr이면 실제 (volid, pageid)를, 아니면 null 위치를 사용한다. 2PC와 end-chkpt 생성자는 각각 두 개의 조건부 복사를 수행하며 error code가 덮어쓰인다 — 마지막 복사가 이긴다.
  5. 복사 헬퍼는 length <= 0 || data == NULL no-op 가드를 갖는 동일한 malloc+memcpy 루틴으로, length 필드가 0이 아닌 경우와 버퍼가 할당된 경우가 일치함을 보장한다.
  6. 통합 오류 경로는 data_header/udata/rdata/node를 해제할 때 progress 플래그 없이 NULL-or-allocated 불변 조건에만 의존한다 — 그것이 사전 초기화가 핵심적인 이유다.
  7. 전체 경로는 prior_lsa_mutexlog_Gl.prior_info에 절대 손대지 않으므로, 노드 구성은 완전히 동시적으로 수행되고 직렬화는 attach 시점(챕터 5)으로 미뤄진다.

Chapter 4: 압축과 Diff 인코딩을 적용한 Undoredo 노드 구성

섹션 제목: “Chapter 4: 압축과 Diff 인코딩을 적용한 Undoredo 노드 구성”

Chapter 3은 바이트를 그대로 복사한다. UNDOREDO는 MVCC의 핫 패스다 — 힙 삽입, 업데이트, 오버플로우 터치가 발생할 때마다 하나씩 생성되며, 레코드 조립 비용이 가장 큰 유형이기도 하다. 단순 복사 위에 세 가지 변환이 겹쳐진다: undo 대비 redo의 diff 인코딩, 진공(vacuum) 메타데이터를 담은 MVCC 스탬핑, 그리고 LZ4 압축 — 이 모든 작업이 prior-list 뮤텍스를 잡기 이전에 완료된다. 이 챕터는 prior_lsa_alloc_and_copy_crumbsprior_lsa_gen_undoredo_record_from_crumbs를 분기 단위로 추적한다. 전체 아키텍처와 뮤텍스 이전 작업이 중요한 이유는 cubrid-prior-list.md의 “The prior list”, “Producer side” 절을 참고한다.

4.1 crumb 인터페이스와 진입 분기

섹션 제목: “4.1 crumb 인터페이스와 진입 분기”

crumb{length, pointer} 쌍이다. 호출자는 undo 또는 redo 이미지를 연속된 하나의 버퍼가 아니라 crumb 배열로 기술하므로, 레코드의 바이트가 흩어진 구조체에서 오더라도 호출자가 직접 이어붙이지 않아도 된다.

// log_crumb -- src/transaction/log_append.hpp
struct log_crumb { int length; const void *data; };

prior_lsa_alloc_and_copy_crumbs가 공개 진입점이다. LOG_PRIOR_NODEmalloc하고, 페이로드 포인터(data_header, udata, rdata)를 NULL로 초기화한 다음 rec_type으로 분기한다: UNDO/REDO/UNDOREDO 계열 타입은 모두 단일 위임 함수로 전달되고, 그 외 타입은 assert_release (false)로 처리된다. LOG_*_DIFF_UNDOREDO_DATA 두 케이스는 switch 안에 존재하지만 호출자로부터 전달되지 않는다 — diff 승격은 내부에서 이루어지며, 위임 함수 자체도 입력 타입이 아직 DIFF 타입이 아님을 assert로 검증한다.

// prior_lsa_alloc_and_copy_crumbs -- src/transaction/log_append.cpp
node->data_header = NULL; node->udata = NULL; node->rdata = NULL; /* <- err path frees iff non-NULL */
switch (rec_type) {
case LOG_UNDOREDO_DATA: case LOG_DIFF_UNDOREDO_DATA: case LOG_UNDO_DATA: case LOG_REDO_DATA:
case LOG_MVCC_UNDOREDO_DATA: case LOG_MVCC_DIFF_UNDOREDO_DATA: case LOG_MVCC_UNDO_DATA: case LOG_MVCC_REDO_DATA:
error = prior_lsa_gen_undoredo_record_from_crumbs (thread_p, node, rcvindex, addr, /* + crumb args */);
break;
default: assert_release (false); error = ER_FAILED; break; /* <- not a data record */
}

성공하면 노드가 반환된다. 오류 시 래퍼는 세 페이로드와 노드를 함께 해제하고, 위임 함수의 error: 레이블은 세 페이로드만 해제하고 노드는 해제하지 않는다. 덕분에 두 정리 경로가 double-free 없이 합성된다.

불변 조건 — 페이로드 포인터는 소유권이 확보되기 전까지 NULL이다.malloc된 페이로드 포인터는 NULL이거나 유효한 상태이며, 절대 stale하지 않다. 진입 시 세 포인터를 무조건 NULL로 초기화하므로, 두 정리 경로 모두 != NULL 검사 후 안전하게 해제한다. 강제 방식: 진입 시 무조건 NULL 초기화 + 각 해제 지점의 != NULL 가드. 위반 시 결과: 래퍼가 위임 함수가 이미 해제한 페이로드를 다시 해제(double-free)하거나, 위임 함수가 할당한 페이로드를 누수시킨다. 이 불변 조건은 이하 모든 오류 경로를 지배한다.

위임 함수는 하나의 데이터 헤더 구조체를 선택하고, 이를 나중에 레코드의 고정 크기 헤드로서 로그 페이지에 복사한다. 레코드 계열은 여섯 가지이며, 이에 대응하는 헤더 구조체도 여섯 가지다 — LOG_REC_UNDOREDO, LOG_REC_UNDO, LOG_REC_REDO, 그리고 MVCC 대응 버전인 LOG_REC_MVCC_UNDOREDO, LOG_REC_MVCC_UNDO, LOG_REC_MVCC_REDO. 모두 동일한 LOG_DATA 로케이터를 내포하며 같은 함수로 채워진다. 비-MVCC 세 가지와 공유 빌딩 블록인 LOG_DATA/LOG_VACUUM_INFO는 아래 표에 정리했다. MVCC 세 가지는 이들을 조합한다: LOG_REC_MVCC_UNDOREDO{ undoredo, mvccid, vacuum_info }, LOG_REC_MVCC_UNDO{ undo, mvccid, vacuum_info }, LOG_REC_MVCC_REDO{ redo, mvccid }vacuum_info가 없으므로 MVCC redo는 vacuum 체인 밖에 있다(4.5).

LOG_DATA — 모든 변형에 내포된 복구 로케이터:

필드역할존재 이유
rcvindex복구(RV) 함수의 인덱스바이트를 재적용할 redo/undo 루틴을 선택
pageid대상 페이지 ID목표 페이지를 지정; 논리 레코드는 NULL_PAGEID
offset페이지 내 오프셋변경이 페이지 어디에 적용되는지
volid대상 페이지의 볼륨 ID볼륨을 지정; 논리 레코드는 NULL_VOLID

LOG_REC_UNDOREDO — 일반(비-MVCC) undoredo, 힙/B-트리 레코드의 일반 형태:

필드역할존재 이유
dataLOG_DATA 로케이터: rcvindex + (volid, pageid, offset)재적용 대상 페이지와 RV 함수 선택
ulengthundo 이미지 길이; 최상위 비트 = MAKE_ZIP_LEN으로 설정한 “압축됨” 플래그롤백 시 복구가 읽을 바이트 수; 플래그가 있으면 inflate 수행
rlengthredo 이미지 길이; 최상위 비트 = “압축됨”순방향 재적용; DIFF 모드에서 redo는 undo와 XOR된 상태

LOG_REC_UNDOLOG_REC_REDO — 단방향 레코드, 동일한 2-필드 구조:

필드역할존재 이유
dataLOG_DATA 로케이터, 위와 동일한 방향에는 로케이터 하나면 충분
length이미지 길이; 최상위 비트 = MAKE_ZIP_LEN으로 설정한 “압축됨”복구가 읽을 바이트 수; 플래그가 있으면 inflate

LOG_VACUUM_INFO — MVCC undo 포함 레코드(UNDO, UNDOREDO)에 추가되어 vacuum이 체인을 순회하게 함:

필드역할존재 이유
prev_mvcc_op_log_lsa이 트랜잭션의 이전 MVCC 연산 LSA; 여기서는 NULL, 뮤텍스 하에서 패치Vacuum이 트랜잭션의 버전을 찾기 위해 따라가는 포인터; 뮤텍스 이전에는 LSA가 미확정(Ch 5)
vfid대상 힙/B-트리 파일의 파일 ID삭제/재사용된 파일 감지; 객체 클래스(재사용 가능 vs 참조 가능) 판별

LOG_REC_MVCC_UNDOREDO — 세 MVCC 헤더 중 가장 무거운 것, 필드별 설명(LOG_REC_MVCC_UNDOundoredo 자리에 LOG_REC_UNDO가 들어가는 동일한 형태; LOG_REC_MVCC_REDOvacuum_info 필드를 생략):

필드역할존재 이유
undoredo내포된 LOG_REC_UNDOREDO일반 레이아웃을 재사용하므로 복구가 헤드를 동일하게 읽을 수 있음
mvccid트랜잭션의 MVCC ID (또는 가장 안쪽 서브-트랜잭션 ID)Vacuum과 스냅샷 가시성 판단에 생성 MVCCID 필요
vacuum_info내포된 LOG_VACUUM_INFOvacuum의 트랜잭션별 작업 목록에 레코드를 연결
flowchart LR
  subgraph mvcc["LOG_REC_MVCC_UNDOREDO"]
    ur["undoredo: LOG_REC_UNDOREDO<br/>data + ulength + rlength"]
    mid["mvccid"]
    vi["vacuum_info<br/>prev_mvcc_op_log_lsa + vfid"]
  end
  ur --> ld["data: LOG_DATA<br/>rcvindex,pageid,offset,volid"]

Figure 4-1. MVCC undoredo 헤더는 일반 헤더, MVCCID, vacuum 메타데이터를 내포한다. 비-MVCC 변형은 undoredo 박스만이며, LOG_REC_MVCC_REDOvacuum_info 박스를 제거한다.

4.3 스레드별 LOG_ZIP 컨텍스트와 can_zip 판단

섹션 제목: “4.3 스레드별 LOG_ZIP 컨텍스트와 can_zip 판단”

위임 함수는 기본 타입을 assert로 확인하고, 두 crumb 배열의 합계를 ulength/rlength로 계산한 뒤, 스레드별 zip 컨텍스트를 가져와서 레코드 계열에 따라 has_undo/has_redo/can_zip을 설정한다.

// prior_lsa_gen_undoredo_record_from_crumbs -- src/transaction/log_append.cpp
assert (node->log_header.type != LOG_DIFF_UNDOREDO_DATA
&& node->log_header.type != LOG_MVCC_DIFF_UNDOREDO_DATA); /* <- DIFF arises internally only */
zip_undo = log_append_get_zip_undo (thread_p); zip_redo = log_append_get_zip_redo (thread_p); /* lazy per-thread alloc */
ulength = 0; for (i=0;i<num_ucrumbs;i++) ulength += ucrumbs[i].length; /* re-total both crumb arrays */
rlength = 0; for (i=0;i<num_rcrumbs;i++) rlength += rcrumbs[i].length;
if (LOG_IS_UNDOREDO_RECORD_TYPE (node->log_header.type)) { has_undo = has_redo = true; /* has both */
can_zip = log_Zip_support && (zip_undo != NULL || ulength == 0) && (zip_redo != NULL || rlength == 0); }
else if (LOG_IS_REDO_RECORD_TYPE (node->log_header.type)) { has_redo = true; can_zip = log_Zip_support && zip_redo; }
else /* UNDO type */ { has_undo = true; can_zip = log_Zip_support && zip_undo; }

log_zipLOG_ZIP 컨텍스트로 압축한다. 세 필드는 압축기와 복사 단계(4.5) 사이의 계약이다:

필드역할존재 이유
data_length저장된 이미지의 길이(LOG_ZIP_SIZE_T 길이 접두사 포함)복사 단계가 prior_lsa_copy_*_data_to_node에 전달하는 바이트 수; data_length < length 성공 판정이 여기에 의존
buf_sizelog_data의 용량log_zip_realloc_if_needed가 지연 성장을 수행하고 버퍼가 이미 충분할 때 realloc을 건너뛰게 함
log_data원본 길이를 접두사로 담은 압축 바이트node->udata/node->rdata에 복사할 버퍼; 접두사로 소비자가 unzip 크기를 결정

log_append_get_zip_undo/_redothread_p->log_zip_undo/_redo를 반환하며, 첫 사용 시 IO_PAGESIZE 크기의 LOG_ZIP을 할당한다(#if !defined(SERVER_MODE) 빌드에서는 파일 범위 static을 사용). 이것이 핵심 설계 결정이다: zip 버퍼가 스레드별이므로 N개의 생산자가 prior-list 뮤텍스 밖에서 병렬로 압축할 수 있다. 뮤텍스 안에서 수행하면 가장 비싼 단계를 직렬화하게 된다. log_Zip_supportPRM_ID_LOG_COMPRESS가 켜진 경우에만 log_append_init_zip이 설정하며, 크기 하한 log_Zip_min_size_to_compress의 기본값은 255이다.

flowchart TD
  S["ulength, rlength 합산"] --> Z{"can_zip이고 어느 쪽이든 >= 255?"}
  Z -- 아니오 --> RAW["raw crumb 복사"]
  Z -- 예 --> R{"data_ptr realloc 성공?"}
  R -- 아니오 --> RAW
  R -- 예 --> B{"양쪽 모두 >= 255?"}
  B -- 예 --> D["log_diff 후 양쪽 zip; redo가 압축되면 is_diff 설정"]
  B -- 아니오 --> O["큰 쪽만 zip"]

Figure 4-2. prior_lsa_gen_undoredo_record_from_crumbs의 압축 결정 흐름; 양방향이 모두 255를 넘을 때만 diff 인코딩이 실행된다.

can_zip이 참이고 어느 한 길이가 255에 도달하면, crumb들이 스레드별 data_ptr로 스테이징된다(log_append_realloc_data_ptr로 확장). realloc이 실패하면 data_ptr는 NULL로 유지되고 zip 블록 전체를 건너뛰어 레코드를 raw로 기록한다.

if (can_zip && (ulength >= MIN || rlength >= MIN)) { /* MIN = log_Zip_min_size_to_compress (255) */
total_length = (ulength>0?ulength:0) + (rlength>0?rlength:0);
if (log_append_realloc_data_ptr (thread_p, total_length)) data_ptr = log_append_get_data_ptr (thread_p);
if (data_ptr != NULL) {
tmp_ptr = data_ptr;
if (ulength >= MIN) { undo_data = data_ptr; /* flatten undo crumbs into data_ptr */
for(i=0;i<num_ucrumbs;i++){ memcpy(tmp_ptr,ucrumbs[i].data,ucrumbs[i].length); tmp_ptr+=ucrumbs[i].length; } }
if (rlength >= MIN) { redo_data = tmp_ptr; /* same loop over rcrumbs after undo */ }
if (ulength >= MIN && rlength >= MIN) { /* BOTH: diff then zip both */
(void) log_diff (ulength, undo_data, rlength, redo_data); /* redo ^= undo, in place */
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; /* diff counts only if redo compressed */
} else { /* ONE side >= MIN: zip it only */
if (ulength >= MIN) is_undo_zip = log_zip (zip_undo, ulength, undo_data);
if (rlength >= MIN) is_redo_zip = log_zip (zip_redo, rlength, redo_data); }
}
}

log_diffMIN(ulength, rlength) 바이트 범위에서 redo에 undo를 XOR하여 제자리에서 변환한다. UPDATE처럼 두 이미지가 거의 같은 경우 결과가 대부분 0으로 채워져 압축 효율이 크게 올라간다. log_zip은 스레드의 LOG_ZIP으로 LZ4 압축을 수행한다. 결과 data_length에는 LOG_ZIP_SIZE_T 길이 접두사가 포함되며, data_length < length(압축 출력 + 접두사가 원본 이미지보다 작음) 성공 판정을 통과하지 못하면 레코드를 raw로 기록한다.

불변 조건 — is_diff는 redo가 압축된 경우에만 설정된다. log_diffredo_data를 제자리에서 변환하지만, is_diff는 양방향 분기 안에서 is_redo_zip 이후에만 true로 설정된다. 강제 방식: 양방향 분기 내 단일 if (is_redo_zip) is_diff = true; 문, 그리고 이후 승격 시의 assert (has_redo && has_undo). 위반 시 결과: undo는 압축되었지만 redo가 압축되지 않았을 때, is_diff가 false를 유지하면 압축되지 않은 redo가 원본 crumb에서 재복사된다(4.5), 변환된 버퍼가 아닌; is_diff를 redo raw 상태에서 설정하면 잘못된 redo를 재적용하게 된다. 설정되면 타입은 LOG_[MVCC_]DIFF_UNDOREDO_DATA로 승격된다.

4.5 헤더 할당, fallthrough 캐스케이드, 최종 복사

섹션 제목: “4.5 헤더 할당, fallthrough 캐스케이드, 최종 복사”

switch가 선택된 구조체(LOG_REC_MVCC_UNDOREDO, LOG_REC_MVCC_UNDO, LOG_REC_MVCC_REDO, LOG_REC_UNDOREDO, 또는 더 좁은 LOG_REC_UNDO/LOG_REC_REDO)에 맞게 data_header_length를 크기 결정하고, 헤더를 malloc한다. OOM이면 error:로 점프한다. 두 번째 switch[[fallthrough]]를 활용해 북킵핑 포인터를 연결하여 MVCC/비-MVCC 쌍 각각을 하나의 블록이 채울 수 있도록 한다:

case LOG_MVCC_UNDOREDO_DATA: case LOG_MVCC_DIFF_UNDOREDO_DATA:
mvcc_undoredo_p = (LOG_REC_MVCC_UNDOREDO *) node->data_header;
vacuum_info_p = &mvcc_undoredo_p->vacuum_info; mvccid_p = &mvcc_undoredo_p->mvccid; /* MVCC-only -> 4.6 */
[[fallthrough]];
case LOG_UNDOREDO_DATA: case LOG_DIFF_UNDOREDO_DATA:
undoredo_p = (type==LOG_UNDOREDO_DATA||type==LOG_DIFF_UNDOREDO_DATA
? (LOG_REC_UNDOREDO *) node->data_header : &mvcc_undoredo_p->undoredo);
data_header_ulength_p = &undoredo_p->ulength; data_header_rlength_p = &undoredo_p->rlength;
log_data_p = &undoredo_p->data; break;

MVCC 케이스는 vacuum_info_p/mvccid_p를 설정한 뒤 fallthrough하여, undoredo_p를 최상위 헤더(비-MVCC) 또는 내포된 undoredo(MVCC)로 resolve한다. UNDO/REDO 분기는 비대칭이다: LOG_MVCC_UNDO_DATAvacuum_info_pmvccid_p 모두를 설정(그리고 fallthrough하여 undo_p resolve)하지만, LOG_MVCC_REDO_DATAmvccid_p만 설정하고 vacuum_info_p는 절대 설정하지 않는다LOG_REC_MVCC_REDO에 해당 필드가 없기 때문이다. 따라서 MVCC undo/undoredo는 vacuum 작업 목록에 연결되고, MVCC redo는 연결되지 않는다.

로케이터는 addr에서 채워진다(pgptr 설정 시 pgbuf_get_vpid_ptr로 실제 (volid, pageid) 획득, 논리 레코드는 NULL_PAGEID/NULL_VOLID). 최종 복사는 4.4의 네 플래그를 사용한다:

if (is_undo_zip) { *data_header_ulength_p = MAKE_ZIP_LEN (zip_undo->data_length); /* zipped: top bit set */
error_code = prior_lsa_copy_undo_data_to_node (node, zip_undo->data_length, zip_undo->log_data); }
else if (has_undo) { *data_header_ulength_p = ulength; /* raw */
error_code = prior_lsa_copy_undo_crumbs_to_node (node, num_ucrumbs, ucrumbs); }
/* redo mirrors: is_redo_zip -> MAKE_ZIP_LEN + copy_redo_data; else has_redo -> copy_redo_crumbs */

raw 헬퍼 prior_lsa_copy_undo_crumbs_to_node/prior_lsa_copy_redo_crumbs_to_node는 crumb 길이를 재합산하고, node->udata/node->rdata를 한 번 malloc하여 모든 crumb을 이어 붙인다(합계가 0이면 아무것도 할당하지 않는다). 대상이 NULL임을 assert로 확인하므로 zip과 raw는 방향별로 상호 배타적이다. zip 헬퍼 prior_lsa_copy_undo_data_to_node/prior_lsa_copy_redo_data_to_nodelength <= 0이면 아무것도 하지 않는다.

4.6 MVCC 스탬핑과 지연된 vacuum LSA

섹션 제목: “4.6 MVCC 스탬핑과 지연된 vacuum LSA”

mvccid_p가 NULL이 아니면(MVCC 타입 한정), TDES에서 MVCCID를 스탬핑한다. 가장 안쪽 서브-트랜잭션 우선이며, NULL TDES나 유효하지 않은 ID는 assert_release (false)로 처리한다:

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; }
*mvccid_p = tdes->mvccinfo.sub_ids.empty () ? tdes->mvccinfo.id : tdes->mvccinfo.sub_ids.back (); /* innermost */

vacuum_info_p가 NULL이 아니면(MVCC undo/undoredo/diff — redo 제외), vfidaddr->vfid에서 복사한다. RVES_NOTIFY_VACUUM은 합법적으로 NULL vfid(VFID_SET_NULL)를 전달할 수 있다. 그 외 MVCC 레코드에서 vfid가 없으면 버그다(assert_release (false)). 이후 back-pointer는 LSA_SET_NULL (&vacuum_info_p->prev_mvcc_op_log_lsa)로 NULL 설정한다.

불변 조건 — 이전 MVCC 연산 LSA는 뮤텍스 이전에 채울 수 없다. prev_mvcc_op_log_lsa는 이 레코드 자체의 LSA가 필요하며, 그 LSA는 prior-list 뮤텍스 하에서 attach될 때만 할당된다. 따라서 함수는 NULL을 기록하고 패치를 뒤로 미룬다(cubrid-prior-list.md Chapter 5). 강제 방식: 여기서의 무조건 LSA_SET_NULL, 실제 값은 뮤텍스 획득 후 attach 시 기록. 위반 시 결과: stale하거나 추측한 LSA가 vacuum을 잘못된 레코드로 체이닝하여, vacuum이 살아있는 버전을 건너뛰거나 dangling 링크를 따라간다. 복사 헬퍼에서 발생하는 오류 코드는 공유 error: 레이블로 떨어지며, 4.1의 NULL 가드에 따라 처리된다.

  1. crumb은 호출자를 레이아웃에서 분리한다. 바이트는 {length, data} 배열(LOG_CRUMB)로 도착하고, 빌더가 평탄화하므로 호출자가 미리 이어붙일 필요가 없다.
  2. 세 가지 변환이 UNDOREDO를 구분한다: XOR diff 인코딩(log_diffLOG_*_DIFF_UNDOREDO_DATA), MVCC 스탬핑(mvccid + LOG_VACUUM_INFO), LZ4 압축(log_zip) — log_Zip_support와 255바이트 하한이 게이트를 제어한다.
  3. 압축은 뮤텍스 이전에 스레드별 LOG_ZIP으로 실행된다 — 핵심 설계 결정: N개의 생산자가 병렬로 압축하며, 뮤텍스 안에서 수행하면 가장 비싼 단계를 직렬화하게 된다. LOG_ZIPdata_length(출력 + 길이 접두사), buf_size, log_data 버퍼를 보유한다.
  4. 여섯 계열, 여섯 헤더 구조체, 최종 타입으로 선택: MVCC undoredo/diff에는 LOG_REC_MVCC_UNDOREDO, 단방향 MVCC에는 LOG_REC_MVCC_UNDO/LOG_REC_MVCC_REDO, 그 외에는 일반 LOG_REC_UNDOREDO/LOG_REC_UNDO/LOG_REC_REDO. LOG_REC_MVCC_REDOvacuum_info가 없어 vacuum 체인 밖에 있다.
  5. MAKE_ZIP_LEN은 길이의 최상위 비트를 오버로드한다 (| 0x80000000) 압축된 이미지를 플래그하기 위해; is_diffredo가 압축될 때만 설정되며, 그 경우에만 redo는 소비자가 역산할 redo XOR undo 형태로 저장된다.
  6. DIFF 타입은 내부 전용; 정리는 두 갈래로 나뉜다 (위임 함수는 페이로드를 해제, 래퍼는 노드를 해제 — “페이로드 포인터는 소유 전 NULL” 불변 조건으로 안전 보장), prev_mvcc_op_log_lsa는 뮤텍스 attach 단계를 위해 NULL 상태로 유지된다(Chapter 5).

Chapter 5: LSN 할당과 뮤텍스 하의 연결

섹션 제목: “Chapter 5: LSN 할당과 뮤텍스 하의 연결”

프로듀서가 도착할 때, 손에 든 LOG_PRIOR_NODE(3–4장에서 완성된)는 start_lsa가 null이고 링크도 없는 상태다. 이 장이 답하는 질문은 하나다: 노드가 완성된 이후, LSN은 어떻게 할당되며, 테일에 어떻게 연결되고, prior_lsa_mutex는 무엇을 보호하는가? 이 모든 일은 prior_lsa_next_record_internal 안에서 일어난다. 프로듀서가 치르는 비용은 O(1)이다 — 커서를 전진시키고, 몇 개의 LSA를 기록하고, 포인터 하나를 연결하면 끝이다. 복사와 플러시는 드레인 쪽의 몫이다(6–8장). LSN 할당의 설계 배경은 상위 companion 문서의 “prior LSA 할당”과 “단일 전역 직렬화 지점” 절을 참고하고, 배압(backpressure)은 9장에서 다룬다.

with_lock 상수만 다르게 전달하는 두 개의 얇은 래퍼다:

// prior_lsa_next_record / _with_lock -- src/transaction/log_append.cpp
LOG_LSA 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); } /* <- internal takes the mutex */
LOG_LSA 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); } /* <- caller already holds it */

WITHOUT_LOCK 경로는 진입 시 prior_lsa_mutex를 획득하고, 리턴 전에 해제하며, 배압 블록(5.9절)도 실행한다. WITH_LOCK 경로는 이 중 어느 것도 하지 않는다 — 뮤텍스를 한 번 잡은 채로 여러 레코드를 원자적으로 이어 붙이고(_with_lock을 반복 호출) 이후 해제하는 호출자를 위한 것이다. 배압은 반드시 뮤텍스를 잡지 않은 상태에서 실행해야 한다.

flowchart TB
  A["with_lock == WITHOUT_LOCK?"] -->|yes| B["prior_lsa_mutex.lock()"]
  A -->|no, 호출자가 뮤텍스 보유| D
  B --> D["prior_lsa_start_append: start_lsa, 트랜잭션 체인, back_lsa"]
  D --> E["start_lsa 복사 출력, vacuum 블록 경계 검사"]
  E --> G["타입 디스패치 스탬핑: MVCC / sysop / commit / abort"]
  G --> H["data_header 전진, udata 후 rdata 추가"]
  H --> J["prior_lsa_end_append, 테일 연결, list_size += bytes"]
  J --> M["with_lock == WITHOUT_LOCK?"]
  M -->|yes| N["잠금 해제 + 배압, 9장"]
  M -->|no| P
  N --> P["num_log_records_written++, start_lsa 반환"]

Figure 5-1 — prior_lsa_next_record_internal의 제어 흐름. lock과 unlock 사이가 크리티컬 섹션이다. num_log_records_written++는 잠금 해제 가드 이후에 위치함에 주목하라 — 트랜잭션 단위 상태이므로 크리티컬 섹션 바깥에서 증가시킨다.

불변 조건 — prior_lsa_mutex는 LSN 할당과 리스트 연결을 완전히 직렬화한다. 논리 로그 주소 공간의 모든 바이트는 이 뮤텍스 내부에서 prior_lsa를 전진시켜 발급되고, 모든 노드는 그 내부에서 prior_list_tail에 연결된다. 이로써 (1) 겹치지 않는 LSA와 (2) start_lsa 오름차순으로 정렬된 리스트 = 플러시 순서가 보장된다. 뮤텍스 바깥에서 전진하거나 연결하면 두 트랜잭션이 같은 바이트 범위를 요구할 수 있다.

5.3 prior_lsa_start_appendstart_lsa, 트랜잭션 체인, back_lsa

섹션 제목: “5.3 prior_lsa_start_append — start_lsa, 트랜잭션 체인, back_lsa”

LSN이 탄생하는 지점이다 — start_lsa는 현재 prior_lsa 값이다:

// prior_lsa_start_append -- src/transaction/log_append.cpp
log_prior_lsa_append_advance_when_doesnot_fit (sizeof (LOG_RECORD_HEADER)); /* <- header must not straddle a page */
node->log_header.trid = tdes->trid;
LSA_COPY (&node->start_lsa, &log_Gl.prior_info.prior_lsa); /* <- THE LSN */
if (tdes->is_system_worker_transaction () && !tdes->is_under_sysop ())
{ LSA_SET_NULL (&node->log_header.prev_tranlsa); /* <- no per-tran undo chain */
LSA_SET_NULL (&tdes->head_lsa); LSA_SET_NULL (&tdes->tail_lsa); }
else
{ LSA_COPY (&node->log_header.prev_tranlsa, &tdes->tail_lsa); /* <- chain back to tran's last */
LSA_COPY (&tdes->tail_lsa, &log_Gl.prior_info.prior_lsa); /* <- record is now tran's tail */
if (LSA_ISNULL (&tdes->head_lsa)) { LSA_COPY (&tdes->head_lsa, &tdes->tail_lsa); } /* <- first record */
LSA_COPY (&tdes->undo_nxlsa, &log_Gl.prior_info.prior_lsa); } /* <- rollback resumes here */
LSA_COPY (&node->log_header.back_lsa, &log_Gl.prior_info.prev_lsa); /* <- physical back link, every record */
LSA_SET_NULL (&node->log_header.forw_lsa); /* <- filled by end_append */
LSA_COPY (&log_Gl.prior_info.prev_lsa, &log_Gl.prior_info.prior_lsa);
log_prior_lsa_append_add_align (sizeof (LOG_RECORD_HEADER)); /* <- reserve header, advance cursor */

물리 체인(back_lsa/forw_lsa)은 양쪽 분기 모두에서 설정되어 복구 시 역방향 스캔이 항상 가능하다. if/else 분기는 트랜잭션 체인에서만 갈라지는데, if 분기(sysop 범위 밖의 system worker, 롤백 없음)는 이를 null로 만든다. 앞부분의 advance_when_doesnot_fit은 스탬핑 전에 실행되므로 start_lsa는 페이지 내부에 헤더가 완전히 담기는 위치를 가리킨다.

start_lsa를 읽은 뒤:

// prior_lsa_next_record_internal -- src/transaction/log_append.cpp
if (LOG_ISRESTARTED () && log_Gl.hdr.does_block_need_vacuum)
{
assert (!LSA_ISNULL (&log_Gl.hdr.mvcc_op_log_lsa));
if (vacuum_get_log_blockid (log_Gl.hdr.mvcc_op_log_lsa.pageid) != vacuum_get_log_blockid (start_lsa.pageid))
{ // ... assert block-id advances monotonically ...
vacuum_produce_log_block_data (thread_p); } /* <- different block: hand the closed one to vacuum */
}

완전히 재시작된 상태이고 이전 MVCC 레코드가 블록을 dirty 상태로 만들었을 때만 발동된다.

5.5 상태 스탬핑 — 타입 디스패치

섹션 제목: “5.5 상태 스탬핑 — 타입 디스패치”

node->log_header.type에 대한 if/else-if 체인으로, 정확히 하나의 arm만 실행되며 대부분의 레코드는 아무것도 스탬핑하지 않는다. 이 타입들은 LSN 할당과 동시에 원자적으로 복구 상태를 기록한다 — 스탬핑이 prior_lsa_mutex 내부에 있는 이유다. 아래 표에 모든 arm이 열거되어 있으며, MVCC와 LOG_SYSOP_END는 비자명한 구조를 보여주는 발췌문을 포함한다.

MVCC arm. 가드는 세 가지 MVCC 데이터 타입, 또는 서브타입이 LOG_SYSOP_END_LOGICAL_MVCC_UNDOLOG_SYSOP_END에서 발동된다. 이후 3방향 내부 디스패치로 내장 struct를 선택한다:

// prior_lsa_next_record_internal -- src/transaction/log_append.cpp
if (node->log_header.type == LOG_MVCC_UNDO_DATA)
{ mvcc_undo = (LOG_REC_MVCC_UNDO *) node->data_header; } /* <- top-level undo cast */
else if (node->log_header.type == LOG_SYSOP_END)
{ mvcc_undo = & ((LOG_REC_SYSOP_END *) node->data_header)->mvcc_undo; } /* <- NESTED member, not a cast */
else /* UNDOREDO / DIFF_UNDOREDO */
{ mvcc_undoredo = (LOG_REC_MVCC_UNDOREDO *) node->data_header; } /* <- undoredo cast */
// vacuum_info and mvccid read from the selected struct, then:
LSA_COPY (&vacuum_info->prev_mvcc_op_log_lsa, &log_Gl.hdr.mvcc_op_log_lsa); /* <- chain to prior MVCC op */
prior_update_header_mvcc_info (start_lsa, mvccid); /* <- advance global MVCC head */

undo와 undoredo/diff는 data_header를 직접 캐스트하지만, LOG_SYSOP_END는 레코드 내부에 내장된 mvcc_undo를 읽는다 — 최상위 캐스트가 아니다. 세 경우 모두 prev_mvcc_op_log_lsa를 연결하고 헤드 업데이트를 호출한다.

Sysop / commit / abort arm은 뮤텍스 하에서 tdes에 복구 상태를 기록해 동시 체크포인트가 일관된 값을 보도록 한다:

type동작뮤텍스 하에 있는 이유
LOG_SYSOP_START_POSTPONEsysop_start_postpone_lsa = start_lsa 설정; 완료된 경우 atomic_sysop_start_lsa 초기화; state = TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE 설정LSA + 상태를 함께 참조 가능
LOG_SYSOP_END두 개의 독립된 조건부 초기화(아래 코드): atomic_sysop_start_lsa null 처리, 이후 sysop_start_postpone_lsa null 처리 — 각각 해당 슬롯이 설정되어 있고 lastparent_lsa < 조건을 만족할 때만중첩 해제를 원자적으로
LOG_COMMIT_WITH_POSTPONE / _OBSOLETEtran_start_postpone_lsa = start_lsa복구가 재개되는 지점
LOG_SYSOP_ATOMIC_STARTatomic_sysop_start_lsa = start_lsa원자적 영역 경계
LOG_COMMIT / LOG_ABORTcommit_abort_lsa = start_lsa결정적 WAL 지점

LOG_SYSOP_END arm은 하나의 결합 조건이 아닌 **두 개의 독립된 if**로 구성된다:

// prior_lsa_next_record_internal -- src/transaction/log_append.cpp
if (!LSA_ISNULL (&tdes->rcv.atomic_sysop_start_lsa)
&& LSA_LT (&sysop_end->lastparent_lsa, &tdes->rcv.atomic_sysop_start_lsa))
{ LSA_SET_NULL (&tdes->rcv.atomic_sysop_start_lsa); } /* <- clear #1, guarded by its own slot */
if (!LSA_ISNULL (&tdes->rcv.sysop_start_postpone_lsa)
&& LSA_LT (&sysop_end->lastparent_lsa, &tdes->rcv.sysop_start_postpone_lsa))
{ LSA_SET_NULL (&tdes->rcv.sysop_start_postpone_lsa); } /* <- clear #2, independent guard */

다른 arm의 assert(예: commit_abort_lsa.is_null ())는 각 슬롯이 페이즈당 한 번씩만 기록됨을 명시한다. 같은 슬롯을 두 번 쓰면 잘못 구성된 트랜잭션이다.

5.6 prior_update_header_mvcc_info — 전역 MVCC 헤드

섹션 제목: “5.6 prior_update_header_mvcc_info — 전역 MVCC 헤드”
// prior_update_header_mvcc_info -- src/transaction/log_append.cpp
assert (MVCCID_IS_VALID (mvccid));
if (!log_Gl.hdr.does_block_need_vacuum) /* <- first MVCC record of this block */
{ log_Gl.hdr.oldest_visible_mvccid = log_Gl.mvcc_table.get_global_oldest_visible ();
log_Gl.hdr.newest_block_mvccid = mvccid; }
else /* <- same block: track max */
{ /* ... sanity asserts ... */
if (log_Gl.hdr.newest_block_mvccid < mvccid) { log_Gl.hdr.newest_block_mvccid = mvccid; } }
log_Gl.hdr.mvcc_op_log_lsa = record_lsa; /* <- this record is now latest MVCC op */
log_Gl.hdr.does_block_need_vacuum = true; /* <- re-arms the flag Section 5.4 reads next */

5.7 data_header 이후 udata, rdata 순서로 커서 전진

섹션 제목: “5.7 data_header 이후 udata, rdata 순서로 커서 전진”

커서는 디스크 순서대로 세 영역을 통과한다 — 헤더는 fit/align 쌍으로, 가변 길이 영역은 prior_lsa_append_data로:

// prior_lsa_next_record_internal -- src/transaction/log_append.cpp
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 = log_Gl.prior_info.prior_lsa):

// log_prior_lsa_append_align -- src/transaction/log_append.cpp
prior_lsa.offset = DB_ALIGN (prior_lsa.offset, DOUBLE_ALIGNMENT);
if ((size_t) prior_lsa.offset >= (size_t) LOGAREA_SIZE) { prior_lsa.pageid++; prior_lsa.offset = 0; } /* <- align rolled page */
// log_prior_lsa_append_advance_when_doesnot_fit -- src/transaction/log_append.cpp
if ((size_t) prior_lsa.offset + length >= (size_t) LOGAREA_SIZE) { prior_lsa.pageid++; prior_lsa.offset = 0; } /* <- push chunk forward */
// log_prior_lsa_append_add_align -- src/transaction/log_append.cpp
prior_lsa.offset += (add); log_prior_lsa_append_align (); /* <- advance then re-align */

LOG_PRIOR_LSA_LAST_APPEND_OFFSETLOGAREA_SIZE를 반환한다. 헤더와 달리 가변 페이로드는 페이지를 걸칠 수 있으므로, prior_lsa_append_data는 페이지 경계를 넘는 루프를 포함한다:

// prior_lsa_append_data -- src/transaction/log_append.cpp
if (length == 0) { return; } /* <- nothing to advance */
log_prior_lsa_append_align ();
current_offset = prior_lsa.offset; last_offset = LOG_PRIOR_LSA_LAST_APPEND_OFFSET ();
if ((current_offset + length) >= last_offset) /* <- crosses >=1 page boundary */
while (length > 0)
{
if (current_offset >= last_offset) /* <- at page end: roll, reset offsets */
{ prior_lsa.pageid++; current_offset = 0; /* ... offset=0 ... */ }
copy_length = (current_offset + length >= last_offset)
? CAST_BUFLEN (last_offset - current_offset) /* <- rest of page */
: length; /* <- final partial chunk */
current_offset += copy_length; length -= copy_length; prior_lsa.offset += copy_length;
}
else { prior_lsa.offset += length; } /* <- fits in page, single bump */
log_prior_lsa_append_align ();

이름과 달리 바이트를 실제로 복사하지 않는다 — 오직 커서만 이동해서 prior_lsardata 마지막 바이트의 한 칸 뒤를 가리키게 한다(실제 복사는 7장).

5.8 prior_lsa_end_append, 테일 연결, list_size

섹션 제목: “5.8 prior_lsa_end_append, 테일 연결, list_size”
// prior_lsa_end_append -- src/transaction/log_append.cpp
log_prior_lsa_append_align ();
log_prior_lsa_append_advance_when_doesnot_fit (sizeof (LOG_RECORD_HEADER)); /* <- next record's header must fit */
LSA_COPY (&node->log_header.forw_lsa, &log_Gl.prior_info.prior_lsa); /* <- forward link = next record's start */

forw_lsa다음 레코드가 받을 주소다. 앞의 advance_when_doesnot_fit이 먼저 실행되므로 다음 헤더에 필요한 페이지 롤을 반영한 뒤 값이 설정된다. 이후 연결과 크기 증가:

// prior_lsa_next_record_internal -- src/transaction/log_append.cpp
if (log_Gl.prior_info.prior_list_tail == NULL) /* <- empty list */
{ log_Gl.prior_info.prior_list_header = node; log_Gl.prior_info.prior_list_tail = node; }
else /* <- append to tail */
{ log_Gl.prior_info.prior_list_tail->next = node; 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);

node->next는 빌드 단계에서 이미 null로 설정되어 있으므로, 어느 분기에서든 해당 노드가 리스트의 끝을 맺는다. list_size는 인메모리 공간(노드 struct + 세 개의 버퍼)을 누적한다 — 드레인 데몬과 5.9절이 메모리 상한과 비교하는 값이다.

5.9 배압 tail-block과 두 가지 결과

섹션 제목: “5.9 배압 tail-block과 두 가지 결과”

배압 블록은 WITHOUT_LOCK 경로의 끝, 뮤텍스가 해제된 이후에 실행된다(9장). _with_lock 호출자는 이를 건너뛴다.

리턴 시 두 가지 사실이 성립한다: (1) 반환된 start_lsa는 레코드의 최종 온디스크 LSN이다 — 뮤텍스 내부에서 단조 증가 커서로 할당되었으므로 결코 바뀌지 않으며, 호출자는 이를 영속 주소로 저장한다. (2) 프로듀서는 WAL 작업을 더 이상 하지 않는다 — 복사와 플러시는 드레인 쪽의 일이다(6–8장).

  1. prior_lsa_next_record_internal이 단일 직렬화 지점이다prior_lsa_mutex가 LSN 할당과 리스트 연결을 완전히 직렬화해, 겹치지 않는 LSA와 start_lsa 오름차순 리스트(= 플러시 순서)를 보장한다.
  2. with_lock이 두 래퍼를 구분한다prior_lsa_next_record는 뮤텍스를 관리하고 배압을 실행하며, _with_lock은 호출자가 이미 뮤텍스를 보유하고 있다고 가정하고 둘 다 건너뛴다.
  3. prior_lsa_start_append는 LSN을 할당하고 두 개의 체인을 유지한다 — 항상 작동하는 물리 체인(back_lsa/forw_lsa)과, non-sysop system worker에서는 null로 만들고 그 외에는 연결하는 트랜잭션 체인.
  4. 타입 디스패치는 LSN과 원자적으로 복구 상태를 기록한다 — MVCC arm의 3방향 디스패치(LOG_SYSOP_END내장된 mvcc_undo를 읽음)와 해당 arm의 두 개의 독립된 조건부 초기화에 주목하라.
  5. 커서 전진은 바이트를 복사하지 않는다 — fit/align/add-align과 prior_lsa_append_data의 페이지 경계 루프는 오직 prior_lsa만 이동시킨다. 복사는 7장에서 이루어진다.
  6. prior_lsa_end_append와 연결이 노드를 완성한다forw_lsa로 물리 체인이 완성되고, 빈 리스트/테일 추가 분기로 노드가 연결되며, list_size에 공간이 누적된다.
  7. 리턴 시 LSN은 확정되고 프로듀서의 역할은 끝난다 — 플러시는 6–8장, 배압은 9장이다.

Chapter 6: 드레인 측에서의 리스트 분리

섹션 제목: “Chapter 6: 드레인 측에서의 리스트 분리”

3~5장에서는 프로듀서 스레드를 따라갔다. 프로듀서는 뮤텍스 밖에서 노드를 구성한 뒤, prior_lsa_mutex를 잡고 노드에 LSN을 기입하고, log_Gl.prior_info를 루트로 하는 공유 단방향 연결 리스트의 꼬리에 노드를 이어 붙였다. 이 장에서는 컨슈머 측으로 시선을 옮겨 하나의 질문에 답한다. 플러시가 실행될 때, 단 한 바이트도 복사하지 않으면서, 진행 중인 append와 경쟁하지 않으면서, 프로듀서가 만들어 놓은 리스트를 어떻게 가져가는가?

답은 의도적으로 작게 설계된 두 함수 쌍에 있다. 외부 함수 logpb_prior_lsa_append_all_list는 굵은 LOG_CS 크리티컬 섹션을 보유한 채 잠깐 prior_lsa_mutex를 잡고 바이트 카운트를 스냅샷으로 읽은 다음, 내부 함수 prior_lsa_remove_prior_list를 호출한다. 내부 함수는 세 번의 포인터 저장과 한 번의 정수 저장이 전부다. 스왑이 끝나면 분리된 체인은 단독 소유자의 전용 리스트가 된다. 다른 스레드에서는 이 리스트를 볼 수 없으므로, 이후 단계(7장의 공식 로그 페이지로의 복사)는 락 없이 실행된다. 이 설계가 작업을 이렇게 분할하는 이유는 cubrid-prior-list §“Producer/consumer split”을 참고하라. 이 장은 포인터 수준에서 핸드오버가 어떻게 이루어지는지를 추적한다.

log_prior_lsa_info는 1장에서 완전히 소개했으므로 여기서 다시 유도하지 않는다. 드레인에 참여하는 필드는 네 개다. 아래에 원문 그대로 인용한다. 역할 매트릭스의 정본은 1장에 있다.

// log_prior_lsa_info -- src/transaction/log_append.hpp
struct log_prior_lsa_info
{
LOG_LSA prior_lsa;
LOG_LSA prev_lsa;
/* list */
LOG_PRIOR_NODE *prior_list_header;
LOG_PRIOR_NODE *prior_list_tail;
INT64 list_size; /* bytes */
/* flush list */
LOG_PRIOR_NODE *prior_flush_list_header;
std::mutex prior_lsa_mutex;
log_prior_lsa_info ();
};
필드드레인에서의 역할존재 이유
prior_list_header호출자에게 반환되는 값. 다음 프로듀서가 새 리스트를 시작할 수 있도록 NULL로 초기화된다.드레인의 전체 페이로드가 이 포인터 하나다 — 체인 전체가 한 번의 읽기로 이전된다.
prior_list_tail다음 append(5장)가 양쪽 끝을 재초기화할 수 있도록 NULL로 초기화된다.초기화하지 않으면 이후의 prior_lsa_next_record가 이미 플러셔에게 넘어간 노드에 이어 붙이게 된다(핸드오버 후 사용).
list_size스왑 직전에 current_size로 스냅샷되고, 이후 0으로 초기화된다.PSTAT_PRIOR_LSA_LIST_SIZE perfmon 카운터를 킬로바이트 단위로 갱신하고, 백프레셔 회계를 재시작한다(9장).
prior_lsa_mutex스냅샷 읽기와 스왑 구간에서만 보유하며, 복사 구간에서는 보유하지 않는다.프로듀서/컨슈머 핸드오버를 원자적으로 유지하면서 락 보유 구간을 O(1)로 억제한다.

불변 조건 (분리 후 단독 소유). prior_lsa_remove_prior_list가 반환된 이후, 반환된 prior_list 포인터로 도달할 수 있는 체인은 다른 어디에서도 도달할 수 없다. prior_list_headerprior_list_tail이 모두 NULL이므로 어떤 프로듀서도 이 리스트에 이어 붙일 수 없고, 어떤 두 번째 드레인도 이 리스트를 관찰할 수 없다. 코드는 헤드를 읽는 것과 세 번의 초기화를 모두 같은 prior_lsa_mutex 락 구간 안에서 수행함으로써 이를 강제한다. 위반될 경우 — 예를 들어 prior_list_tail이 분리된 체인을 가리킨 채 남아 있다면 — 동시에 실행 중인 prior_lsa_next_record가 플러셔가 이미 순회 중인 리스트에 새 노드를 연결하여 로그 스트림을 손상시킨다.

6.2 logpb_prior_lsa_append_all_list — 드레인 진입점

섹션 제목: “6.2 logpb_prior_lsa_append_all_list — 드레인 진입점”

이 함수가 드레인 측의 유일한 공개 심볼이다(log_impl.hextern으로 선언). 전체를 인용할 수 있을 만큼 짧다.

// 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;
INT64 current_size;
assert (LOG_CS_OWN_WRITE_MODE (thread_p));
log_Gl.prior_info.prior_lsa_mutex.lock ();
current_size = log_Gl.prior_info.list_size;
prior_list = prior_lsa_remove_prior_list (thread_p);
log_Gl.prior_info.prior_lsa_mutex.unlock ();
if (prior_list != NULL)
{
perfmon_add_stat (thread_p, PSTAT_PRIOR_LSA_LIST_SIZE, (unsigned int) current_size / ONE_K); /* kbytes */
perfmon_inc_stat (thread_p, PSTAT_PRIOR_LSA_LIST_REMOVED);
logpb_append_prior_lsa_list (thread_p, prior_list);
}
return NO_ERROR;
}

분기 완전 분석 — 분기는 두 개뿐이지만 모두 중요하다.

  1. 전제 조건 assert. LOG_CS_OWN_WRITE_MODE (thread_p)(log_manager.h에 선언)는 호출자가 이미 LOG_CS 크리티컬 섹션을 쓰기 모드로 보유하고 있음을 단언한다. 이는 디버그 빌드 가드이지 락 획득이 아니다. 함수 자체는 LOG_CS를 직접 잡지 않으며 호출자를 신뢰한다. §6.4에 나오는 모든 호출자는 LOG_CS_ENTER … LOG_CS_EXIT로 이 호출을 감싼다.

  2. 내부 락 + 스냅샷 + 분리. prior_lsa_mutex.lock()을 잡은 상태에서 list_size를 로컬 current_size에 복사한 뒤(스냅샷이 분리될 체인과 일관성을 갖도록 락을 보유한 채 읽는다), prior_lsa_remove_prior_list가 스왑을 수행하고 이전 헤드를 반환한다. 뮤텍스는 직후에 해제된다. 락 보유 구간은 정확히 스냅샷 읽기 더하기 네 번의 저장 뿐이며, 바이트 복사는 포함되지 않는다.

  3. 빈 리스트 분기 (prior_list == NULL). 마지막 드레인 이후 어떤 프로듀서도 append하지 않았다면, 분리된 헤드는 NULL이다. 함수는 perfmon 갱신과 복사를 모두 건너뛰고 NO_ERROR를 반환한다. 유휴 서버에서의 일반적인 경로이며, 플러시 데몬의 wakeup 비용을 낮게 유지한다.

  4. 비어 있지 않은 분기 (prior_list != NULL). 두 perfmon 통계가 발화된 뒤 복사가 진행된다. PSTAT_PRIOR_LSA_LIST_SIZEcurrent_size / ONE_K를 누적한다 — 스냅샷을 킬로바이트로 변환한 값(정수 나눗셈이므로 1 KB 미만 드레인은 0으로 기록된다). PSTAT_PRIOR_LSA_LIST_REMOVED는 비어 있지 않은 드레인 횟수를 세는 단순 카운터다. 카운터 갱신이 끝난 뒤에야 제어가 logpb_append_prior_lsa_list(7장)로 넘어가고, 이 함수는 이제 전용 소유가 된 체인을 순회하며 각 노드를 공식 로그 페이지에 복사한다. 이 호출의 반환값은 여기서 무시된다 — logpb_prior_lsa_append_all_list는 항상 NO_ERROR를 반환하며, 복사 측 실패는 복사 경로 내부의 logpb_fatal_error로 에스컬레이션되며, 이 반환 코드를 거치지 않는다.

flowchart TD
  A["logpb_prior_lsa_append_all_list"] --> B["LOG_CS_OWN_WRITE_MODE assert"]
  B --> C["prior_lsa_mutex.lock"]
  C --> D["current_size = list_size  (스냅샷)"]
  D --> E["prior_list = prior_lsa_remove_prior_list"]
  E --> F["prior_lsa_mutex.unlock"]
  F --> G{"prior_list != NULL ?"}
  G -- "없음, 유휴" --> H["return NO_ERROR"]
  G -- "있음" --> I["perfmon_add PRIOR_LSA_LIST_SIZE = current_size/ONE_K kB"]
  I --> J["perfmon_inc PRIOR_LSA_LIST_REMOVED"]
  J --> K["logpb_append_prior_lsa_list  -> 7장 복사"]
  K --> H

Figure 6-1. 드레인 진입점의 제어 흐름. 내부 락은 C-F 구간에서만 보유하며, K의 복사는 락 없이 실행된다.

6.3 prior_lsa_remove_prior_list — 최소한의 스왑

섹션 제목: “6.3 prior_lsa_remove_prior_list — 최소한의 스왑”

내부 함수가 실제 핸드오버를 수행한다. static으로 선언되어 있으며 두 락이 이미 보유되어 있다고 가정한다. 외부 락은 assert로 확인하고, 내부 락은 호출자가 보유하고 있음을 묵시적으로 신뢰한다.

// 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;
log_Gl.prior_info.prior_list_tail = NULL;
log_Gl.prior_info.list_size = 0;
return prior_list;
}

루프도 분기도 없다 — 단일 경로다. prior_list_header를 로컬 변수에 읽어 들인 뒤 정확히 네 번의 저장을 수행한다.

  • prior_list_header = NULL — 다음 프로듀서의 append가 NULL 헤드를 보고 리스트를 재초기화한다(5장의 if (prior_list_tail == NULL) 분기가 발화된다).
  • prior_list_tail = NULL — 동일한 재초기화를 위해 재무장된다.
  • list_size = 0 — 바이트 회계를 영점에서 재시작한다.

저장 횟수를 정확하게 세는 것이 설계 주장에 중요하다. 세 번의 포인터 저장과 한 번의 INT64 저장, 페이로드 바이트 이동은 없다. 메가바이트에 달할 수 있는 로그 레코드 체인 전체가 헤드 포인터 하나를 재할당함으로써 소유권을 바꾼다. thread_p 인자는 LOG_CS_OWN_WRITE_MODE assert를 위해서만 존재하며 그 외에는 사용되지 않는다. log_prior_lsa_infoprev_lsa, prior_lsa, prior_flush_list_header 필드는 의도적으로 건드리지 않는다 — prior_lsa/prev_lsa는 프로듀서가 소유하는 실행 중인 LSN 커서로 드레인 간에 살아남아야 하며(5장), prior_flush_list_header는 다른 리스트에 속하므로 이 스왑과 무관하다.

stateDiagram-v2
  [*] --> 공유됨
  공유됨 --> 분리됨: prior_lsa_mutex 획득 \n 헤드 읽기, 4번 저장
  note right of 공유됨
    header -> n1 -> n2 -> ... -> tail
    list_size = 합계(bytes)
    모든 프로듀서에게 가시
  end note
  note right of 분리됨
    header = NULL, tail = NULL, size = 0
    반환된 prior_list -> n1 -> ... -> tail
    플러셔에게만 가시
  end note
  분리됨 --> 공유됨: 다음 프로듀서 append가 header와 tail 재초기화

Figure 6-2. 스왑 전후 리스트의 소유권 상태. 체인 내부의 링크는 결코 재작성되지 않는다 — log_prior_lsa_info의 루트 포인터만 이동한다.

6.4 두 락의 순서와 세 드레인 호출자

섹션 제목: “6.4 두 락의 순서와 세 드레인 호출자”

불변 조건 (락 순서: 외부 LOG_CS 먼저, 내부 prior_lsa_mutex 두 번째, 잠깐). 드레인에 도달하는 모든 경로는 진입 전에 LOG_CS를 쓰기 모드로 보유하며, 드레인은 그 구간 안에서 prior_lsa_mutex를 잡았다가 복사 전에 해제한다. 중첩 순서는 항상 LOG_CSprior_lsa_mutex이며 역순은 없다. 위반될 경우 — 어떤 경로가 prior_lsa_mutex를 잡은 상태에서 LOG_CS에 진입하려 한다면 — LOG_CS를 보유한 채 prior_lsa_mutex를 기다리는 스레드와의 락 순서가 역전되어 데드락 위험이 생긴다. 프로듀서 측(5장)은 prior_lsa_mutex만 보유하며 그 상태에서 LOG_CS를 향해 위로 올라가지 않으므로 부분 순서는 전체 순서다.

이 분리 방식이 분리와 복사를 별개로 유지하는 이유이기도 하다. prior_lsa_mutex는 프로듀서 측에서 커밋하는 모든 트랜잭션이 경합하는 락이다(5장은 각 레코드의 LSN을 기입할 때 이 락을 잡는다). 멀티 페이지 바이트 복사 구간에서 이 락을 보유하면 모든 프로듀서가 멈춘다. 먼저 분리하고 O(1)에 락을 해제하면 프로듀서는 즉시 새로운 리스트를 만들기 시작하고, 플러셔는 이전의 이제 전용이 된 리스트를 락 없이 복사할 수 있다.

이 장에서 중요한 드레인 경로는 세 가지이며, 각각 자체 LOG_CS_ENTER/LOG_CS_EXIT로 감싸거나 소유권을 assert한다. 포인터 수준 열거:

  1. 로그 플러시 데몬. log_flush_execute(log_manager.c)가 LOG_CS_ENTER를 호출한 뒤 logpb_flush_pages_direct(log_page_buffer.c)를 호출하고, 이것이 다시 logpb_prior_lsa_append_all_list를 호출한다. 정상 상태 경로다. 프로듀서가 log_Flush_has_been_requested를 올려 log_Flush_daemon을 깨우면 데몬이 드레인 후 플러시한다. 데몬 본체 — 대기 루프, 플러시 후 그룹 커밋 브로드캐스트 — 는 8장이다. 여기서는 드레인을 호출하는 진입점임을 확인하는 데 그친다.

    // log_flush_execute -- src/transaction/log_manager.c
    LOG_CS_ENTER (&thread_ref);
    logpb_flush_pages_direct (&thread_ref);
    LOG_CS_EXIT (&thread_ref);
  2. 직접 플러시. logpb_flush_pages_direct(log_page_buffer.c)는 데몬을 기다릴 수 없는 트랜잭션이 동기적으로 호출하기도 한다 — 단일 프로세스 모드, 서버 미재시작, 데몬 사용 불가, 또는 명시적 logpb_flush_pages 폴백. 이 함수의 LOG_CS_OWN_WRITE_MODE assert 직후 첫 번째 구문이 드레인이다.

    // logpb_flush_pages_direct -- src/transaction/log_page_buffer.c
    assert (LOG_CS_OWN_WRITE_MODE (thread_p));
    logpb_prior_lsa_append_all_list (thread_p);
    (void) logpb_flush_all_append_pages (thread_p);

    호출자 1과 호출자 2 모두 이 동일한 함수를 경유한다 — 데몬은 log_flush_execute를 거쳐 이 함수에 도달한다. logpb_flush_pages_direct의 직접 호출자는 이미 LOG_CS를 보유하고 있다.

  3. 백프레셔 / 부분 레코드 자가 플러시. 두 프로듀서 측 경로가 logpb_flush_pages_direct를 거치지 않고 드레인에 직접 도달한다.

    • prior_lsa_next_record_internal(log_append.cpp): 리스트가 logpb_get_memsize()를 초과하고 데몬 경로를 사용할 수 없을 때(크래시 복구 또는 SA_MODE), 프로듀서가 직접 드레인한다 — LOG_CS_ENTER, 그 다음 logpb_prior_lsa_append_all_list, 그 다음 LOG_CS_EXIT. 이것이 9장에서 자세히 다루는 백프레셔 밸브다.

      // prior_lsa_next_record_internal -- src/transaction/log_append.cpp
      LOG_CS_ENTER (thread_p);
      logpb_prior_lsa_append_all_list (thread_p);
      LOG_CS_EXIT (thread_p);
    • heap_get_visible_version_from_log(heap_file.c): prev_version_lsa가 prior 리스트에서 아직 플러시되지 않은 이전 MVCC 버전을 읽을 때, 리더가 드레인을 강제로 수행하여 버전을 가져올 수 있게 한다 — LOG_CS_ENTER, logpb_prior_lsa_append_all_list, LOG_CS_EXIT 순으로 실행한 뒤 append LSA를 다시 읽고 해당 버전이 이제 내구성을 갖게 되었음을 assert한다.

세 경로 모두 동일한 중첩 순서를 따르며, 모두 prior_lsa_remove_prior_list동일한 스왑에 도달한다. 드레인은 의도적으로 호출자에 무관하도록 설계되어 있다. 왜 호출되었는지 알지 못하고 알 필요도 없다.

각주 (범위 외). 위 세 호출 지점 외에 네 번째 직접 호출 지점이 존재한다. logpb_fetch_page(log_page_buffer.c)는 자체 LOG_CS_ENTER/LOG_CS_EXIT 구간 안에서 로그 페이지를 가져오기 전에 자가 드레인을 수행하여, 요청된 페이지가 아직 복사되지 않은 prior 레코드를 반영하도록 보장한다. 동일한 스왑에 도달하지만 이 장의 범위 밖으로 미룬다.

  1. 드레인은 두 함수다. logpb_prior_lsa_append_all_list가 공개 진입점이다(LOG_CS를 assert, prior_lsa_mutex를 잡아 list_size를 스냅샷하고 스왑을 호출한 뒤 뮤텍스를 해제하고 perfmon과 복사를 수행). prior_lsa_remove_prior_list는 스왑 자체다.
  2. 스왑은 바이트를 이동하지 않는다. 세 번의 포인터 저장(prior_list_header, prior_list_tailNULL로) 과 한 번의 INT64 저장(list_size = 0)이 전부이며, 이전 헤드를 반환한다. 메가바이트 체인이 포인터 하나 재할당으로 소유권을 바꾼다.
  3. 진입 함수의 분기는 두 가지뿐이다. 빈 리스트는 perfmon과 복사를 건너뛰고 NO_ERROR를 반환한다. 비어 있지 않은 경우 PRIOR_LSA_LIST_SIZE(kB)와 PRIOR_LSA_LIST_REMOVED를 기록한 뒤 logpb_append_prior_lsa_list로 넘긴다.
  4. 단독 소유자 불변 조건. 스왑 후 분리된 체인은 log_prior_lsa_info의 어디에서도 도달할 수 없으므로, 다운스트림 복사(7장)는 락 없이 실행되며 어떤 프로듀서도 이 리스트에 이어 붙일 수 없다.
  5. 락 순서는 항상 LOG_CSprior_lsa_mutex다. 드레인은 호출자가 LOG_CS를 보유하고 있음을 신뢰하고(디버그 assert), 내부 뮤텍스는 O(1) 스왑에서만 잡으며 복사 구간에서는 잡지 않는다 — 프로듀서 지연을 최소화한다.
  6. 세 호출자, 하나의 스왑. 로그 플러시 데몬(log_flush_executelogpb_flush_pages_direct 경유), 직접/동기 플러시(logpb_flush_pages_direct), 자가 백프레셔/부분 레코드 경로 (prior_lsa_next_record_internal, heap_get_visible_version_from_log) 모두 동일한 분리에 수렴한다. 데몬 본체와 이들이 트리거하는 복사/플러시는 각각 8장과 7장이다.

Chapter 7: 노드 바이트를 권위 있는 로그 페이지에 복사하기

섹션 제목: “Chapter 7: 노드 바이트를 권위 있는 로그 페이지에 복사하기”

drain 측은 prior_lsa_mutex를 잡은 상태에서 producer 큐를 분리하고 (Chapter 6), 다른 스레드가 볼 수 없는 LOG_PRIOR_NODE 단방향 연결 리스트를 직접 보유한다. 이제 남은 작업은 producer가 하지 못하도록 금지되어 있던 단계, 즉 malloc된 노드에서 레코드 바이트를 꺼내어 LOG_CS_OWN_WRITE_MODE 하에서 LSN 순서대로 권위 있는 LOG_PAGE 링 버퍼에 옮기는 일이다. 이것이 동반 문서(cubrid-prior-list.md §“Flush — making it durable”)에서 설명한 두 단계 분리의 큐→페이지 절반이며, 페이지→디스크 절반은 Chapter 8에서 다룬다.

독자가 품어야 할 질문: 분리된 노드 안의 바이트가 실제 LOG_PAGE 버퍼에 최종적으로 어떻게 기록되는가, 그리고 각 노드는 언제 해제되는가? 이 장에서는 logpb_append_prior_lsa_list(순회·해제 루프)와 logpb_append_next_record (노드 단위 복사)를 추적하고, 이어서 기본 연산인 logpb_start_append / logpb_append_data / logpb_end_append와 페이지 경계 전환을 담당하는 logpb_next_append_page를 살펴본다.

7.1 순회·해제 루프 — logpb_append_prior_lsa_list

섹션 제목: “7.1 순회·해제 루프 — logpb_append_prior_lsa_list”

분리된 리스트는 logpb_append_prior_lsa_list에 전달된다. 이 함수는 리스트를 두 번째 전역 변수 prior_flush_list_header에 staging한 뒤 노드를 하나씩 소비한다.

// 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_CS_OWN_WRITE_MODE (thread_p)); /* <- outer log CS held */
/* append prior_flush_list */
assert (log_Gl.prior_info.prior_flush_list_header == NULL); /* <- single-drain guard */
log_Gl.prior_info.prior_flush_list_header = list; /* <- stage under a named global */
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; /* <- advance BEFORE copy */
logpb_append_next_record (thread_p, node); /* <- copy bytes into LOG_PAGE */
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); /* <- node and its three buffers reclaimed here */
}
return NO_ERROR;
}

prior_flush_list_header는 단순한 로컬 순회 커서가 아니라 계약/디버그 마커다. 코드 전체를 grep해보면 동시 접근하는 reader가 없다(선언, 초기화, 그리고 이 함수 내부의 다섯 곳만 존재한다). 이 전역 변수가 존재하는 이유는 두 가지다. (a) 진입 시 assert(prior_flush_list_header == NULL)로 단일 drain 계약을 검증하고, (b) drain 중간에 core dump를 떴을 때 어느 레코드가 복사되는 중이었는지 log_Gl에서 바로 볼 수 있는 가시성을 확보한다. 헤더는 복사 이전에 node->next로 전진한다.

불변식 — drain은 한 번에 하나만 실행된다. 진입 시의 assert (prior_flush_list_header == NULL)이 프로세스 내 강제 지점이다. 이 불변식은 LOG_CS_OWN_WRITE_MODE에 의해 구조적으로 유지된다 — 로그 크리티컬 섹션을 write 모드로 보유한 스레드(데몬, self-help drain, forced flush 중 하나; cubrid-prior-list.md §“The drain side” 참조)만이 이 함수에 도달할 수 있다. 위반되면 두 번째 대입이 헤더를 덮어써 첫 번째 리스트를 고아로 만들고 페이지 커서를 이중으로 구동하며, §7.2의 start_lsa == append_lsa assertion이 터진다.

각 노드는 바이트가 복사된 직후 즉시 해제된다. 노드 풀은 없으며, 세 개의 payload 버퍼와 노드 자체가 free_and_init으로 C 할당자에 반환된다. 각각 NULL 검사로 보호되는데, 이는 §7.2의 조건부 복사와 대칭을 이룬다. 스레드를 교차하는 malloc/free 비대칭성(producer가 malloc, drain이 free)은 동반 문서의 Open Question 3에서 다룬다.

flowchart TD
  A["진입: list = 분리된 prior 리스트\nassert prior_flush_list_header == NULL"] --> B["prior_flush_list_header = list"]
  B --> C{"prior_flush_list_header != NULL ?"}
  C -- 아니오 --> Z["return NO_ERROR"]
  C -- 예 --> D["node = prior_flush_list_header\nprior_flush_list_header = node->next"]
  D --> E["logpb_append_next_record(node)\n바이트를 LOG_PAGE에 복사"]
  E --> F["data_header / udata / rdata 해제\n(NULL이 아닌 경우)"]
  F --> G["free_and_init(node)"]
  G --> C

Figure 7-1 — 순회·해제 루프. staged-list 헤더는 단일 drain assert와 디버거 가시성을 위한 전역 변수이며, 각 노드는 페이지 버퍼에 바이트가 기록되는 즉시 해제된다.

7.2 노드 하나 복사 — logpb_append_next_record

섹션 제목: “7.2 노드 하나 복사 — 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)) /* <- order sanity, fatal */
{ logpb_fatal_error (thread_p, true, ARG_FILE_LINE, "logpb_append_next_record"); }
/* forcing flush in the middle of log record append is a complicated business. */
if (log_Gl.flush_info.num_toflush + 1 >= log_Gl.flush_info.max_toflush) /* <- pre-flush guard */
{ logpb_flush_all_append_pages (thread_p); } /* <- empty the toflush array first */
log_Gl.append.appending_page_tde_encrypted = prior_is_tde_encrypted (node); /* <- stamp TDE intent */
logpb_start_append (thread_p, &node->log_header); /* <- write the LOG_RECORD_HEADER */
if (node->data_header != NULL)
{
LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT (thread_p, node->data_header_length); /* <- keep header whole */
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); /* <- finalize, validate forw_lsa */
log_Gl.append.appending_page_tde_encrypted = false; /* <- reset TDE intent */
return NO_ERROR;
}

start_lsa == append_lsa assertion은 drain 전체의 핵심 검사다. node->start_lsa는 producer가 prior_lsa_mutex를 잡은 상태에서 기록한 값이며 (Chapter 5), log_Gl.hdr.append_lsa는 다음 바이트가 기록될 페이지 측 커서다. 불일치가 발생하면 큐와 페이지 버퍼의 동기화가 깨진 것이므로 logpb_fatal_error를 호출한다. 복구 분기는 없다.

불변식 — 페이지 커서는 prior-list LSN을 정확히 추적한다. 모든 노드에서 진입 시 node->start_lsa == log_Gl.hdr.append_lsa가 성립하고, 종료 시 header->forw_lsa == log_Gl.hdr.append_lsa가 성립한다(§7.3). 두 assertion이 복사 전후를 lock-step으로 감싼다. 이 불변식이 유지되는 이유는 producer가 start_lsa/forw_lsa를 페이지 커서와 동일한 {pageid, offset} 산술로 계산하고(log_prior_lsa_append_* in log_append.cpp), drain이 노드를 attach 순서대로 처리하기 때문이다. 위반되면 디스크 레코드의 자기 기술 LSA가 물리적 위치와 맞지 않아, recovery의 forw_lsa/back_lsa 체인이 깨진다.

pre-flush guard(num_toflush + 1 >= max_toflush)는 이 레코드를 시작하기 전에 toflush[]를 지금 바로 flush한다(logpb_flush_all_append_pages, Chapter 8). 레코드 중간에 flush를 강제하는 것은 “복잡한 일(a complicated business)“이다 — LOGPB_APPENDREC_STATUS의 partial-record 처리가 필요해진다. 레코드 사이에 drain하는 방식으로 이 일반 경로를 깨끗하게 유지한다.

나머지 분기: appending_page_tde_encrypted는 레코드 중간에 새 페이지가 생성될 때(§7.5) 해당 페이지가 암호화되도록 미리 표시하며, 종료 시 초기화된다. 세 개의 logpb_append_data 호출은 버퍼가 NULL이 아닌 경우에만, 디스크 순서대로 data_header → udata → rdata 순서로 실행된다. data header에만 LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT 사전 전진이 적용되어 한 페이지 내에 온전히 기록되도록 보장하며, udata/rdata는 logpb_append_data 자체가 페이지 경계를 넘어가도록 처리한다(§7.4). 이 함수는 항상 NO_ERROR를 반환하며, 데이터 경로의 실패는 logpb_fatal_error로 상향된다.

logpb_start_append는 고정 크기의 LOG_RECORD_HEADER를 페이지에 기록하고 레코드를 연다.

// 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 LOG_CS write mode, perfmon_inc_stat ... */
LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT (thread_p, sizeof (LOG_RECORD_HEADER)); /* <- header stays whole */
if (!LSA_EQ (&header->back_lsa, &log_Gl.append.prev_lsa)) /* <- back-link sanity, fatal */
{ logpb_fatal_error (thread_p, true, ARG_FILE_LINE, "logpb_start_append"); }
/* ... condensed: assert log_pgptr != NULL, set TDE algorithm on page if appending_page_tde_encrypted ... */
log_rec = (LOG_RECORD_HEADER *) LOG_APPEND_PTR (); /* <- page ptr at append_lsa.offset */
*log_rec = *header; /* <- struct copy header into page */
if (log_Gl.append.log_pgptr->hdr.offset == NULL_OFFSET) /* <- first record on page? */
{ log_Gl.append.log_pgptr->hdr.offset = (PGLENGTH) log_Gl.hdr.append_lsa.offset; }
if (log_rec->type == LOG_END_OF_LOG) /* <- EOF marker, flush only; no IN_PROGRESS */
{
/* this comes from logpb_flush_all_append_pages */
assert (log_Pb.partial_append.status == LOGPB_APPENDREC_SUCCESS
|| log_Pb.partial_append.status == LOGPB_APPENDREC_PARTIAL_ENDED); /* <- two legal prestates */
LSA_COPY (&log_Gl.hdr.eof_lsa, &log_Gl.hdr.append_lsa);
logpb_set_dirty (thread_p, log_Gl.append.log_pgptr);
}
else
{
assert (log_Pb.partial_append.status == LOGPB_APPENDREC_SUCCESS); /* <- no record open */
LSA_COPY (&log_Gl.append.prev_lsa, &log_Gl.hdr.append_lsa); /* <- this LSA -> prev */
LOG_APPEND_SETDIRTY_ADD_ALIGN (thread_p, sizeof (LOG_RECORD_HEADER)); /* <- past header */
log_Pb.partial_append.status = LOGPB_APPENDREC_IN_PROGRESS; /* <- open record */
}
}

각 분기를 살펴보면: advance 매크로가 새 페이지로 전환하여(§7.5) 헤더가 분리되지 않도록 보장한다. fatal back_lsa == prev_lsa 검사는 header->back_lsa를 물리적으로 앞 레코드의 LSA에 묶는다. 페이지의 첫 번째 레코드라면 hdr.offset을 기록하여 recovery가 레코드 시작 위치를 찾을 수 있게 한다. LOG_END_OF_LOG 분기 — EOF 마커는 오직 flusher(Chapter 8)만 기록한다 — 는 LOGPB_APPENDREC_SUCCESS(완료된 레코드) 또는 LOGPB_APPENDREC_PARTIAL_ENDED(레코드 중간 강제 flush 이후 경로) 어느 쪽에서도 올 수 있으며, append_lsaeof_lsa로 복사하고 레코드를 열지 않은 채 페이지를 dirty로 표시한다. 일반 분기는 현재 LSA를 prev_lsa로 기록하고 헤더를 넘어 전진한 뒤 IN_PROGRESS를 연다.

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)
{
LOG_APPEND_ALIGN (thread_p, LOG_DONT_SET_DIRTY);
LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT (thread_p, sizeof (LOG_RECORD_HEADER)); /* <- next hdr fits */
assert (LSA_EQ (&header->forw_lsa, &log_Gl.hdr.append_lsa)); /* <- forw_lsa points to next record */
if (!LSA_EQ (&log_Gl.append.prev_lsa, &log_Gl.hdr.append_lsa))
{ logpb_set_dirty (thread_p, log_Gl.append.log_pgptr); }
if (log_Pb.partial_append.status == LOGPB_APPENDREC_IN_PROGRESS) { /* normal: fall through */ }
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); /* <- re-flush the now-complete record */
assert (log_Pb.partial_append.status == LOGPB_APPENDREC_PARTIAL_FLUSHED_ORIGINAL); /* <- re-flush left it here */
}
else { assert_release (false); } /* <- invalid state */
log_Pb.partial_append.status = LOGPB_APPENDREC_SUCCESS;
}

이 함수는 다음 헤더가 맞을 자리를 확보하여 append_lsa가 다음 레코드 시작점을 가리키게 한다. forw_lsa == append_lsa assertion은 §7.2의 start_lsa 검사가 열었던 lock-step 괄호를 닫는다. 상태 분기는 보통 IN_PROGRESS이며, PARTIAL_FLUSHED_END_OF_LOG는 레코드 중간 강제 flush(Chapter 8) 이후에만 도달하고 완성된 레코드를 재flush한다. 그 외는 assert_release(false)다. 최종적으로 상태는 SUCCESS로 돌아온다.

7.4 payload 바이트 복사 — logpb_append_data

섹션 제목: “7.4 payload 바이트 복사 — logpb_append_data”

logpb_append_data는 바이트 이동자다 — drain 전체에서 payload를 페이지 경계를 넘어 기록하는 유일한 지점이다.

// 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; } /* <- nothing to copy */
LOG_APPEND_ALIGN (thread_p, LOG_DONT_SET_DIRTY);
ptr = LOG_APPEND_PTR (); /* <- write cursor in page */
last_ptr = LOG_LAST_APPEND_PTR (); /* <- end of usable area */
if ((ptr + length) >= last_ptr) /* <- payload spans page boundary */
{
while (length > 0)
{
if (ptr >= last_ptr) /* <- page exhausted: cross, dirty old page */
{
logpb_next_append_page (thread_p, LOG_SET_DIRTY);
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 /* <- fits in one page */
{
memcpy (ptr, data, length);
log_Gl.hdr.append_lsa.offset += length;
}
LOG_APPEND_ALIGN (thread_p, LOG_SET_DIRTY); /* <- align next, dirty page */
}

세 가지 분기가 있다. 빈 데이터 가드는 즉시 반환한다. else 분기는 일반 경우로 memcpy 한 번과 offset 증가 한 번으로 끝난다. while 분기는 payload가 last_ptr에 닿을 때 실행되며, 들어갈 수 있는 만큼 복사하고 logpb_next_append_page(§7.5)를 호출해 페이지를 건너간 뒤 데이터가 소진될 때까지 반복한다. LOG_APPEND_PTR()log_pgptr->area + append_lsa.offset으로 해석되므로, 이 memcpy들은 staging 버퍼 없이 살아있는 LOG_PAGE 프레임에 직접 기록된다. 이것이 바로 producer가 prior_lsa_mutex 하에서 하지 못하도록 금지되었던 작업이며, LOG_CS 하의 단일 drain에서만 실행된다.

7.5 페이지 경계 전환 — logpb_next_append_page

섹션 제목: “7.5 페이지 경계 전환 — logpb_next_append_page”

advance/align 매크로나 logpb_append_data의 spanning 루프에서 발생하는 모든 페이지 경계 전환은 logpb_next_append_page를 거친다. 이 함수는 방금 완료된 페이지를 dirty로 표시하고, 다음 페이지를 할당하며, flush_info.toflush[]에 등록한다.

// logpb_next_append_page -- src/transaction/log_page_buffer.c
static void
logpb_next_append_page (THREAD_ENTRY * thread_p, LOG_SETDIRTY current_setdirty)
{
LOG_FLUSH_INFO *flush_info = &log_Gl.flush_info;
bool need_flush;
if (current_setdirty == LOG_SET_DIRTY)
{ logpb_set_dirty (thread_p, log_Gl.append.log_pgptr); } /* <- dirty the page we are leaving */
log_Gl.append.log_pgptr = NULL;
log_Gl.hdr.append_lsa.pageid++; /* <- next logical page */
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); }
if (LOGPB_IS_FIRST_PHYSICAL_PAGE (log_Gl.hdr.append_lsa.pageid)) { /* ring wrap: bump fpageid, flush header */ }
log_Gl.append.log_pgptr = logpb_create_page (thread_p, log_Gl.hdr.append_lsa.pageid);
if (log_Gl.append.log_pgptr == NULL)
{ logpb_fatal_error (thread_p, true, ARG_FILE_LINE, "log_next_append_page"); return; }
/* ... condensed: if appending_page_tde_encrypted, set TDE algorithm on new page ... */
rv = pthread_mutex_lock (&flush_info->flush_mutex);
flush_info->toflush[flush_info->num_toflush] = log_Gl.append.log_pgptr; /* <- register page to flush */
flush_info->num_toflush++;
need_flush = (flush_info->num_toflush >= flush_info->max_toflush);
pthread_mutex_unlock (&flush_info->flush_mutex);
if (need_flush) { logpb_flush_all_append_pages (thread_p); } /* <- toflush array full */
}

분기별로 살펴보면: 떠날 때 dirty 처리LOG_SET_DIRTY(spanning 루프)는 떠나는 페이지를 dirty로 표시하고, LOG_DONT_SET_DIRTY는 호출자가 이미 처리했음을 의미한다. 아카이브 / 링 랩 — 다음 아카이브 페이지 ID와 충돌하면 먼저 아카이빙한다. 첫 번째 물리 페이지라면 fpageid를 증가시키고 헤더를 flush한다 (로그 링 하우스키핑, log-manager 동반 문서 참조). logpb_create_page 실패는 fatal이다. TDEappending_page_tde_encrypted가 설정된 상태에서 생성된 페이지에는 TDE 알고리즘이 적용된다. 등록 및 조건부 flush — 프레임이 flush_mutex 하에 toflush[]에 추가되며, max_toflush에 도달하면 여기서 flush를 강제한다. 이것이 §7.2의 guard가 앞서서 방지하려는 바로 그 임계값이다.

  1. logpb_append_prior_lsa_list는 분리된 리스트를 순회하며 각 노드의 바이트가 복사되는 즉시 해제한다. 전역 prior_flush_list_header에 리스트를 staging하는 것은 단일 drain 계약 마커(진입 시 assert(... == NULL)) 및 디버거 가시성을 위함이며, 실제 직렬화는 write 모드의 LOG_CS가 담당한다.

  2. logpb_append_next_record는 fatal start_lsa == append_lsa 검사로 시작한다 — prior 리스트의 LSN과 페이지 커서가 lock-step으로 맞아떨어짐을 증명하는 불변식이다. 불일치 시 서버를 중단하며, 복구 분기는 없다.

  3. pre-flush guard(num_toflush + 1 >= max_toflush)는 레코드 사이에 toflush[]를 비운다 — 비용이 큰 레코드 중간 강제 flush를 피하기 위해서다. 레코드 중간 강제 flush의 partial-record 처리는 logpb_start_append / logpb_end_appendLOGPB_APPENDREC_* 상태 머신이 담당한다.

  4. 노드의 세 버퍼는 NULL이 아닌 경우에만 추가되며, 디스크 순서대로 data_header → udata → rdata 순으로 기록된다. data header는 한 페이지에 온전히 기록되도록 사전 전진이 적용되고, logpb_append_data가 udata/rdata의 페이지 경계 spanning을 처리한다.

  5. logpb_start_append는 헤더를 struct copy하고 레코드를 연다(IN_PROGRESS). logpb_end_appendforw_lsa == append_lsa를 검증하고 닫는다(SUCCESS). EOF 분기는 완료된 레코드 또는 PARTIAL_ENDED 상태 어느 쪽에서도 올 수 있다.

  6. logpb_next_append_page는 모든 페이지 전환의 단일 진입점이다 — 떠나는 페이지를 dirty 처리하고, 다음 페이지를 할당하고, TDE 의도를 전파하며, 새 프레임을 flush_info.toflush[]에 등록한다(가득 차면 flush를 강제한다). drain은 LOG_APPEND_PTR()로 staging 버퍼 없이 살아있는 LOG_PAGE 프레임에 직접 복사하므로, toflush[]와 페이지별 dirty 플래그가 Chapter 8 디스크 writer로 가는 유일한 핸드오프다.

Chapter 8: 디스크 플러시와 커밋 대기자 깨우기

섹션 제목: “Chapter 8: 디스크 플러시와 커밋 대기자 깨우기”

이 챕터는 다음 질문에 답한다: 노드가 페이지 버퍼에 들어간 이후, 어떻게 페이지가 내구성을 갖추고, nxio_lsa가 어떻게 전진하며, 파킹된 커미터가 자신의 레코드가 디스크에 기록됐음을 어떻게 알게 되는가? Ch.6/7은 각 노드의 페이로드를 메모리에서 더티 상태인 LOG_PAGE에 남겨 두었고, nxio_lsa(내구성 워터마크)는 마지막 쓰기가 끝난 위치를 여전히 가리키고 있다. 상위 컴패니언 문서인 cubrid-prior-list.md의 §“Commit waiters” 4분면 표에 정책이 정리되어 있으며, 이 챕터는 그 정책을 전제로 각 분기를 추적한다. 이 플러시가 WAL 파이프라인 전체(체크포인트, 아카이브, 복구 스캔)에서 차지하는 위치는 cubrid-log-manager-detail.md를 교차 참조하라.

8.1 드레인 후 쓰기 시퀀서: logpb_flush_pages_direct

섹션 제목: “8.1 드레인 후 쓰기 시퀀서: logpb_flush_pages_direct”

모든 내구성 플러시 경로는 하나의 시퀀서로 집약된다:

// logpb_flush_pages_direct -- src/transaction/log_page_buffer.c
assert (LOG_CS_OWN_WRITE_MODE (thread_p)); /* <- caller already holds the log CS */
logpb_prior_lsa_append_all_list (thread_p); /* <- Ch.6/7: drain queue into LOG_PAGE buffer */
(void) logpb_flush_all_append_pages (thread_p); /* <- this chapter: write pages, advance nxio_lsa */

불변 조건: 드레인이 쓰기보다 먼저 실행되어야 하며, (비-HA 경로에서는) 호출자가 전체 시퀀스 동안 LOG_CS 쓰기 모드를 보유한다. 드레인을 먼저 수행하지 않으면 nxio_lsa가 prior list에 아직 남아 있는 레코드를 지나쳐 전진하고, 그 레코드를 기다리는 커미터가 깨어나 커밋을 잃는 사고가 발생한다. “CS를 전체 구간 동안 보유”하는 조건은 비-HA에만 해당된다. HA 핸드오프(§8.2.6)는 쓰기 루프 전후에 CS를 *강등(demote)*한다. logpb_force_flush_pages는 CS를 갖지 않은 호출자를 위해 LOG_CS_ENTER → direct → LOG_CS_EXIT 순으로 CS를 감싸 준다.

8.2 logpb_flush_all_append_pages — 디스크 쓰기 절반

섹션 제목: “8.2 logpb_flush_all_append_pages — 디스크 쓰기 절반”

가장 큰 함수로, 더티 상태의 flush_info->toflush[] 페이지를 활성 로그에 쓰고, fsync를 수행하며, nxio_lsa를 전진시킨다. 복잡도의 대부분은 크래시 안전 북키핑에서 비롯된다. Figure 8-1이 권위 있는 분기 맵이다.

flowchart TB
  B{"num_toflush<1 or\n단독 클린 페이지?"} -->|yes| R0["return 0"]
  B -->|need_flush| E{"partial_append.status?"}
  E -->|IN_PROGRESS| E1["페이지 복사본에 EOF 기록, 복사본 쓰기,\n-> PARTIAL_FLUSHED_END_OF_LOG"]
  E -->|PARTIAL_FLUSHED_EOL| E2["새 마커 없음"]
  E -->|PARTIAL_ENDED or SUCCESS| E3["버퍼에 logpb_start_append EOF"]
  E -->|other| ERR["assert_release, 오류"]
  E1 --> HA["HA 활성화? LOG_CS_DEMOTE, LOGWR 깨우기"]
  E2 --> HA
  E3 --> HA
  HA --> F["루프: 클린+nxio 건너뜀, 더티 런 수집,\nwritev_append_pages"]
  F --> G{"nxio 페이지 플러시?\nSUCCESS or nxio != prev_lsa"}
  G -->|yes| G1["nxio 페이지 쓰기, need_sync=true"]
  G -->|no| G2["건너뜀: nxio는 미완성 헤더"]
  G1 --> H{"need_sync?"}
  G2 --> H
  H -->|yes| H1["fileio_synchronize\n(SUPPRESS_FSYNC 기준)"]
  H -->|no| BA["백그라운드 아카이빙? to_archive"]
  H1 --> BA
  BA --> I{"status?"}
  I -->|PARTIAL_ENDED| J1["헤더 복원, 재쓰기+fsync,\nnxio=append_lsa, -> FLUSHED_ORIGINAL"]
  I -->|PARTIAL_FLUSHED_EOL| J2["nxio = append.prev_lsa"]
  I -->|SUCCESS| J3["nxio = hdr.append_lsa"]
  I -->|other| ERR
  J1 --> K["num_toflush=0, log_pgptr 재추가,\nHA 활성화? 라이터 알림, LOG_CS_PROMOTE"]
  J2 --> K
  J3 --> K
  K --> R1["return 1"]

Figure 8-1 — logpb_flush_all_append_pages의 분기 맵. 두 partial_append.status 스위치가 쓰기 루프와 선택적 HA 핸드오프를 양쪽에서 감싼다.

8.2.1 조기 종료. flush_info->flush_mutex 하에서: num_toflush < 1이거나, 1이면서 해당 페이지가 더티하지 않을 경우(logpb_is_dirty), return 0을 반환한다 — 유휴 틱에서 불필요한 마커 재기록을 방지한다. 반환 규약: 1 = 플러시 완료, 0 = 플러시 불필요, < 0 = 오류. 내구성은 nxio_lsa에 의해 보장되므로 호출자들은 반환값을 (void)로 캐스팅한다.

8.2.2 로그 끝 마커 — partial 처리의 상단 절반. 복구의 전방 스캔이 내구적 로그의 끝을 알 수 있도록 로그는 반드시 LOG_END_OF_LOG 마커로 끝나야 한다. 마커 처리는 log_Pb.partial_append.status에 따라 달라진다:

status의미마커 처리
..._IN_PROGRESS플러시 강제 시점에 레코드가 append 진행 중헤더 페이지를 복사하고, 레코드를 LOG_END_OF_LOG로 덮어씀(forw_lsa null 처리), 복사본을 씀, hdr.eof_lsa = append.prev_lsa 설정, → PARTIAL_FLUSHED_END_OF_LOG
..._PARTIAL_FLUSHED_END_OF_LOG임시 EOF가 이미 디스크에 존재아무 처리 없음
..._PARTIAL_ENDED / ..._SUCCESS레코드 append 완료버퍼에 새 LOG_END_OF_LOGlogpb_start_append함 (로그 주소 전진 없음)
그 외예기치 않은 상태assert_release (false); goto error

IN_PROGRESS 분기는 오버사이즈 레코드에 대한 크래시 안전의 핵심이다:

// logpb_flush_all_append_pages -- src/transaction/log_page_buffer.c (IN_PROGRESS, condensed)
memcpy (log_Pb.partial_append.log_page_record_header, bufptr->logpage, LOG_PAGESIZE);
bufptr->dirty = false; /* <- keep fake EOF off the real page */
LSA_SET_NULL (&log_Pb.partial_append.record_header_p->forw_lsa); /* <- no forward pointer past EOF */
log_Pb.partial_append.record_header_p->type = LOG_END_OF_LOG; /* (original header saved for §8.2.5) */

가짜 EOF는 디스크 복사본에만 기록되고, 버퍼 페이지는 실제 레코드를 유지하며 클린으로 표시되어 쓰기 루프가 건너뛰게 된다.

8.2.3 2단계 쓰기 루프 — nxio 페이지를 마지막으로.

불변 조건: nxio_lsa 페이지는 마지막에 쓰인다. 복구는 해당 페이지가 디스크에 기록된 후에야 새로운 로그 끝을 유효한 것으로 인정한다. 따라서 플러시 중 크래시가 발생하면 이전 로그 끝(nxio 페이지 미기록)이나 새로운 로그 끝(모든 대상 페이지 이후에 기록됨)만 남고, forw_lsa가 디스크에 닿지 않은 레코드 본문을 가리키는 상황은 발생하지 않는다.

루프(Figure 8-1 노드 F)는 클린 페이지와 nxio 페이지를 건너뛰고, 페이지가 더티하면서 논리적·물리적 pageid가 연속인 동안 런(run)을 확장한다. 클린, nxio, 논리적 갭, 물리적 갭 — 네 가지 조건이 런을 끊는다. 이 갭 검사 덕분에 logpb_writev_append_pages 한 번으로 런 전체를 벡터 쓰기로 처리할 수 있다. 그런 다음 nxio 페이지를 단독으로 쓰는데, 단 미완성 레코드의 헤더 페이지인 경우(status != SUCCESS이고 nxio 페이지 == log_Gl.append.prev_lsa.pageid)에는 이미 가짜 EOF를 담고 있으므로 §8.2.5로 미룬다. SUCCESS 상태에서는 신선한 로그 끝 마커를 담고 있어, 새 테일을 확정하기 위해 마지막에 기록된다.

8.2.4 fsync와 백그라운드 아카이빙. need_sync가 참이고 (PRM_ID_SUPPRESS_FSYNC == 0 또는 total_sync_count % PRM_ID_SUPPRESS_FSYNC == 0)이면, fileio_synchronize (thread_p, log_Gl.append.vdes, log_Name_active, false)를 호출한다. SUPPRESS_FSYNC = 0이면 매 플러시마다 sync하고, 0이 아니면 N번째마다 sync한다. 이 지점에서 내구성이 물리적으로 확보된다. 이후 PRM_ID_LOG_BACKGROUND_ARCHIVING이 설정되어 있으면, logpb_write_toflush_pages_to_archive가 플러시된 페이지를 미리 할당된 아카이브 볼륨에 미러링한다(아카이브 로그 생성이 rename으로 완료되도록). 이는 nxio_lsa 경로에서 벗어난 사이드 쓰기다.

8.2.5 nxio_lsa 전진 — partial 처리의 하단 절반. 여기서 설정되는 값이 쓰기자-대기자 계약이다: 커미터(§8.4)가 관찰하는 값이다.

// logpb_flush_all_append_pages -- src/transaction/log_page_buffer.c (nxio advance, condensed)
if (log_Pb.partial_append.status == LOGPB_APPENDREC_PARTIAL_ENDED) { /* oversized record now complete */
*log_Pb.partial_append.record_header_p = log_Pb.partial_append.original_record_header; /* restore real header */
logpb_write_page_to_disk (...); fileio_synchronize (...); /* rewrite + sync again */
log_Gl.append.set_nxio_lsa (log_Gl.hdr.append_lsa); /* <- now safe: real record durable */
log_Pb.partial_append.status = LOGPB_APPENDREC_PARTIAL_FLUSHED_ORIGINAL;
} else if (log_Pb.partial_append.status == LOGPB_APPENDREC_PARTIAL_FLUSHED_END_OF_LOG)
log_Gl.append.set_nxio_lsa (log_Gl.append.prev_lsa); /* <- cannot pass incomplete record yet */
else if (log_Pb.partial_append.status == LOGPB_APPENDREC_SUCCESS)
log_Gl.append.set_nxio_lsa (log_Gl.hdr.append_lsa); /* <- normal: watermark = append cursor */
else { assert_release (false); goto error; }

set_nxio_lsa는 원자 저장(log_append_infostd::atomic<LOG_LSA>)이므로, CS 없이 get_nxio_lsa로 읽는 커미터도 일관된 값을 본다. PARTIAL_ENDED의 이중 쓰기 — §8.2.2의 가짜 EOF, 그리고 여기서 실제 헤더를 복원하고 재sync — 는 dangling forw_lsa를 원천적으로 차단한다. 워터마크가 append_lsa에 도달하는 것은 실제 레코드가 내구적으로 저장된 이후에만 가능하다.

8.2.6 HA 라이터 핸드오프. 서버 모드에서 HA가 활성화되고 라이터 플러시가 억제되지 않은 경우(!HA_DISABLED () && !writer_info->skip_flush), 함수는 쓰기 루프 전후에 로그 CS를 읽기 모드로 *강등(demote)*하고, 대기 중인 각 LWT 엔트리를 LOGWR_STATUS_FETCH로 전환한 후 스레드를 깨운다:

// logpb_flush_all_append_pages -- src/transaction/log_page_buffer.c (HA hand-off, condensed)
LOG_CS_DEMOTE (thread_p); /* <- write -> read: LOGWR threads read the log concurrently */
entry->status = LOGWR_STATUS_FETCH; /* <- tell each waiting LWT: go fetch */
thread_wakeup_already_had_mutex (wait_thread_p, THREAD_LOGWR_RESUMED);

강등은 마커가 결정된(§8.2.2) 이후, 쓰기 루프 이전에 이루어진다. 쌍을 이루는 완료 블록(Figure 8-1 노드 K)은 writer_info->flush_wait_cond를 브로드캐스트하고, 모든 LOGWR_STATUS_FETCH 엔트리가 소진될 때까지 기다린 뒤 LOG_CS_PROMOTE로 복귀한다. 이것이 §8.1의 “CS를 전체 구간 동안 보유”에 대한 유일한 예외다. LOG_CS_DEMOTE/LOG_CS_PROMOTE는 매크로가 아닌 log_manager.c의 함수이며, HA가 비활성화되면 이 쌍 전체가 건너뛰어진다.

서버 모드에서 플러시는 보통 log-flush 데몬이 담당한다. 본문은 짧고 모든 줄이 게이트 역할을 한다:

// log_flush_execute -- src/transaction/log_manager.c
if (!BO_IS_SERVER_RESTARTED () || !log_Flush_has_been_requested)
return; /* <- short-circuit: empty tick does no work */
LOG_CS_ENTER (&thread_ref);
logpb_flush_pages_direct (&thread_ref); /* <- drain + write + advance nxio_lsa */
LOG_CS_EXIT (&thread_ref);
pthread_mutex_lock (&group_commit_info.gc_mutex);
pthread_cond_broadcast (&group_commit_info.gc_cond); /* <- wake ALL committers */
log_Flush_has_been_requested = false; /* <- clear the request flag */
pthread_mutex_unlock (&group_commit_info.gc_mutex);

분기별 설명: (1) !BO_IS_SERVER_RESTARTED () — 부팅/복구가 아직 완료되지 않아 반환(복구는 자체 플러시). (2) !log_Flush_has_been_requested — 루퍼는 타이머 주기로 틱을 발생시키며, 요청이 없으면 LOG_CS를 잡지 않고 반환한다(따라서 상위 문서의 “INF, woken by commits”는 다소 부정확하다 — 실제로는 빈 틱을 단락시키는 타이머 기반 루퍼다). (3) LOG_CS 하에서 플러시. (4) CS 해제 후, gc_mutex 하에서 브로드캐스트하고 플래그를 초기화한다. §8.4의 set과 쌍을 이뤄 요청이 누락되지 않는다. CS 내부의 원자적 nxio_lsa 저장이 브로드캐스트를 release fence로 만든다.

불변 조건: 브로드캐스트는 플러시가 완료된 시점에 대기 중이던 모든 커미터를 포괄한다. pthread_cond_broadcast(signal이 아님)가 모든 대기자를 깨워 nxio_lsa < flush_lsa를 재평가하게 한다. fsync 한 번이 nxio_lsa를 한 번 전진시키고, 브로드캐스트 한 번이 해당 코호트 전체를 해방시킨다.

8.4 데몬 깨우기: log_wakeup_log_flush_daemon과 주변 함수들

섹션 제목: “8.4 데몬 깨우기: log_wakeup_log_flush_daemon과 주변 함수들”

커미터(또는 Ch.9의 배압 생산자)는 log_wakeup_log_flush_daemon으로 플러시를 요청한다. log_is_log_flush_daemon_available ()(log_Flush_daemon != NULL — SA 모드와 init 이전에는 false)이 참이면, log_Flush_has_been_requested = true먼저 설정한 뒤 log_Flush_daemon->wakeup ()을 호출한다. wakeup() 이전에 플래그(std::atomic_bool)를 설정하면 레이스를 막는다. 주기 함수(§8.5)도 이 플래그를 읽어 대기 중일 때 슬립을 0으로 줄이므로, wakeup()이 유실되어도 플러시가 지연되지 않는다.

8.4.1 커미터 대기 루프: logpb_flush_pages. logpb_flush_pages(flush_lsa)는 커미터의 진입점이다. 분기 완전 탐색: (0) SA 빌드(#if !defined(SERVER_MODE))와 두 가지 서버 모드 런타임 우회 — !BO_IS_SERVER_RESTARTED () || flush_lsa == NULL || LSA_ISNULL, 그리고 !log_is_log_flush_daemon_available () — 모두 §8.4.2의 직접 플러시로 라우팅된다. (1) 그 외에는 need_wait = (PRM_ID_LOG_ASYNC_COMMIT == false)need_wakeup_LFT = (LOG_IS_GROUP_COMMIT_ACTIVE () == false)를 설정한다 — 상위 문서 매트릭스의 (async, group) 4분면. need_wait이 참인(동기) 두 분면만 CV 루프에 진입하고, 비동기 쌍은 즉시 반환한다(단, !group이면 데몬 깨우기 후). (2) CV 루프는 while (LSA_LT (&nxio_lsa, flush_lsa))로 스핀한다: gc_mutex 잠금, get_nxio_lsa ()로 재확인, 조건부 log_wakeup_log_flush_daemon, pthread_cond_timedwait (&gc_cond, &gc_mutex, ...) 1초 대기, 언락, wakeup_LFT=true. 슬립 전에 gc_mutex 하에서 재확인함으로써 §8.3 브로드캐스트와의 lost-wakeup 레이스를 막는다. need_wakeup_LFT는 그룹 커밋 레버다: sync+group 모드에서는 초기값이 false이므로 첫 번째 반복에서 데몬을 깨우지 않고 파킹하여, 타이머 틱(§8.5)이 도착을 배치로 묶도록 한다. 영구 데이터 페이지 latch(pgbuf_has_perm_pages_fixed) 보유 중이거나 타임아웃 이후의 반복에서는 강제로 데몬을 깨운다.

불변 조건: 동기 커미터는 nxio_lsa >= flush_lsa가 될 때만 반환한다. §8.2.5의 규칙 — nxio_lsafileio_synchronize 이후에만 전진 — 과 결합하면, 어떤 트랜잭션도 LOG_COMMIT이 안정 스토리지에 기록되기 전에 커밋 응답을 받지 않는다는 보장이 된다. 비동기 분면은 이를 깨뜨린다(need_wait = false) — PRM_ID_LOG_ASYNC_COMMIT이 크래시 시 안전하지 않은 이유다.

8.4.2 비서버 / 미재시작 우회. SA 빌드는 LOG_CS_ENTERlogpb_flush_pages_directLOG_CS_EXIT로만 컴파일된다. 서버 모드에서는 두 가지 런타임 가드가 CV 루프를 우회하여 직접 플러시로 라우팅한다: !BO_IS_SERVER_RESTARTED () || flush_lsa == NULL || LSA_ISNULL(부팅/복구 — §8.3도 어차피 단락됨), 그리고 !log_is_log_flush_daemon_available (). 핵심적인 assert (!LOG_CS_OWN_WRITE_MODE (thread_p))는 두 우회 사이에 위치한다 — 재시작 우회 이후, 데몬 가용성 우회 이전 — 실제로 파킹하는 경로를 보호한다. 커미터는 로그 CS를 보유한 채 대기해서는 안 되며, 그렇지 않으면 데몬과 교착 상태에 빠진다.

log_flush_daemon_init주기 함수 log_get_log_group_commit_interval(고정 인터벌이 아님)로 cubthread::looper를 구성하고, log_flush_execute를 본문으로 하는 데몬을 "log-flush"라는 이름으로 create_daemon한다:

// log_get_log_group_commit_interval -- src/transaction/log_manager.c (condensed)
if (log_Flush_has_been_requested) { period = milliseconds (0); return; } /* <- pending: tick now */
period = milliseconds (msec == 0 ? 1000 : msec); /* <- 0 means default 1s cap */

주기가 함수 포인터이므로, PRM_ID_LOG_GROUP_COMMIT_INTERVAL_MSECS를 변경하면 재시작 없이 다음 사이클부터 반영된다. 대기 중인 요청이 있으면 주기를 0 ms로 줄여(lost wakeup()을 극복), 0 기본값에서는 유휴 상한이 1초로 제한되어 그룹 커밋 배치 윈도우를 경계 짓는다.

  1. logpb_flush_pages_direct는 유일한 시퀀서다: CS 쓰기 모드를 단언하고, prior list를 드레인한 뒤 쓰기를 수행한다. drain-before-write는 강제 불변 조건으로, 위반 시 nxio_lsa가 휘발성 메모리에 남은 레코드를 지나쳐 전진할 수 있다.
  2. nxio_lsa는 내구성 워터마크로, logpb_flush_all_append_pages 내부에서 fileio_synchronize 이후 원자적 set_nxio_lsa로만 전진한다. SUCCESS이면 append_lsa로 점프하고, 두 partial 상태는 미완성 레코드를 넘어서지 않도록 핀/복원한다.
  3. 2단계 페이지 쓰기는 크래시 안전 댄스다: nxio_lsa 페이지(즉 SUCCESS 시 신선한 로그 끝 마커)를 마지막에 씀으로써, 크래시 시 이전 로그 끝이나 새 로그 끝만 남고 dangling forw_lsa는 남지 않는다. 오버사이즈 레코드는 partial_append.status로 추적되는 가짜-EOF/실제-헤더 이중 쓰기를 추가한다.
  4. HA 복제는 쓰기 루프를 LOG_CS_DEMOTE / LOG_CS_PROMOTE로 감싼다(LOGWR 엔트리를 LOGWR_STATUS_FETCH로 전환, 드레인, 프로모트). 백그라운드 아카이빙은 fsync 이후 플러시된 페이지를 미러링한다.
  5. log_flush_execute는 데몬 본문이다: 재시작 완료 log_Flush_has_been_requested가 아니면 단락; LOG_CS 하에서 플러시; gc_mutex 하에서 gc_cond를 브로드캐스트하고 플래그를 초기화 — 원자적 nxio_lsa 저장 이후의 브로드캐스트가 release fence다.
  6. 커미터는 logpb_flush_pages에서 파킹한다: 4분면 (async, group) 정책에 따라 동기 분면만 gc_cond CV 루프에 진입하고, nxio_lsa >= flush_lsa가 될 때만 반환 — 커밋 응답은 곧 내구적 LOG_COMMIT을 의미한다.
  7. 데몬의 주기는 함수다(log_get_log_group_commit_interval): 요청 대기 중이면 0 ms(lost wakeup 방어), 그 외에는 설정된 인터벌(상한 1초).

Chapter 9: 배압(Backpressure)과 자기 구제 소진(Self-Help Drain)

섹션 제목: “Chapter 9: 배압(Backpressure)과 자기 구제 소진(Self-Help Drain)”

이 챕터가 답하는 독자 질문: 프로듀서가 flush 데몬이 소진할 수 있는 속도보다 빠르게 prior list를 채울 때 무슨 일이 일어나는가?

lock-free 프로듀서 경로(3~5챕터)를 밟는 prior_lsa_next_record_internal의 모든 호출은 동일한 꼬리 블록으로 끝난다. 노드를 연결하고 list_size를 올린 뒤, 인메모리 prior list가 로그 페이지 버퍼 크기를 넘었는지 확인한다. 초과했다면 프로듀서는 소프트 배압을 적용한다 — flush 데몬을 깨우고 타임슬라이스를 양보하거나, 데몬이 없는 경우에는 직접 리스트를 소진한다. 이것이 프로듀서 경로에서 자신이 삽입한 단일 레코드가 아닌 큐의 전체 깊이에 반응하는 유일한 지점이다.

이 챕터는 그 꼬리 블록을 분기별로 해부한다. prior list가 왜 존재하는지, flush가 어떻게 동작하는지에 대한 상위 수준 설명은 cubrid-prior-list.md의 §“The producer/consumer split”과 §“Write-ahead logging”에서 다루었으므로, 이 챕터는 그것을 전제한다. 새로운 구조체는 도입되지 않는다.

9.1 소프트 상한선: logpb_get_memsize()

섹션 제목: “9.1 소프트 상한선: logpb_get_memsize()”

임계값은 logpb_get_memsize가 매 확인마다 새로 계산한다:

// logpb_get_memsize -- src/transaction/log_page_buffer.c
size_t
logpb_get_memsize ()
{
return (size_t) log_Pb.num_buffers * (size_t) LOG_PAGESIZE;
}

log_Pb.num_buffers는 부트 시 logpb_initialize_pool에서 log_max_buffers 시스템 파라미터(PRM_ID_LOG_NBUFFERS)로부터 한 번 설정된다:

// logpb_initialize_pool -- src/transaction/log_page_buffer.c
log_Pb.num_buffers = prm_get_integer_value (PRM_ID_LOG_NBUFFERS);

따라서 logpb_get_memsize()인메모리 로그 페이지 버퍼의 총 바이트 용량 — 챕터 7에서 노드를 복사하는 바로 그 풀 — 을 반환한다. 설계상 prior list 소프트 상한선을 이 풀의 크기에 연동한 이유가 있다: 복사 + flush 파이프라인이 결국 메모리를 한정하는 요소이므로, 대기 중인 prior list가 그 버퍼보다 훨씬 크게 자라도록 내버려둘 이유가 없다. 상한선은 list_size와 동일한 단위인 바이트로 표현된다. 챕터 5에서 확인했듯 list_size는 페이지 버퍼가 포함하지 않는 LOG_PRIOR_NODE 오버헤드도 포함하므로, 데이터가 버퍼를 실제로 채우기 전에 임계값을 넘는다 — 프로듀서가 일찍 반응하도록 의도된 보수적 설계다.

불변 조건 — 상한선은 소프트이며 프로듀서를 절대 하드 블록하지 않는다. logpb_get_memsize()를 초과해도 프로듀서가 노드를 연결하는 것이 막히지는 않는다. 노드는 이미 연결되었고 list_size는 이미 올라간 뒤(챕터 5)에 이 확인이 실행된다. 이 확인은 프로듀서가 소진을 돕느냐 아니면 양보하느냐를 결정할 뿐이다. 이 시그널을 완전히 무시한 프로듀서도 여전히 정확하게 동작하며 — 리스트가 그냥 자랄 뿐이다. 시스템의 실제 하드 흐름 제어 한계는 데이터 페이지에 대한 WAL 불변 조건(더티 데이터 페이지는 로그를 앞질러 flush될 수 없다; cubrid-prior-list.md §“Write-ahead logging” 참조)이며, 그것이 결국 트랜잭션을 정체시킨다. 이 블록은 압력 완화 밸브이지 게이트가 아니다.

flowchart LR
  subgraph prod["프로듀서 (뮤텍스 미보유 상태)"]
    A["list_size += 노드 바이트<br/>(뮤텍스 하에서, Ch.5)"] --> B["prior_lsa_mutex 해제"]
    B --> C{"list_size >=<br/>logpb_get_memsize() ?"}
  end
  C -->|아니오| Z["return start_lsa"]
  C -->|예| D["PSTAT_PRIOR_LSA_LIST_MAXED++"]
  D --> E["빌드/런타임 모드별 분기<br/>(Figure 9-2)"]
  E --> Z

Figure 9-1. 배압 확인이 뮤텍스 기준으로 어디에 위치하는지. 이 확인은 WITHOUT_LOCK 프로듀서 경로에서만, 그리고 뮤텍스가 해제된 이후에만 실행된다.

9.2 운영자가 볼 수 있는 신호: PSTAT_PRIOR_LSA_LIST_MAXED

섹션 제목: “9.2 운영자가 볼 수 있는 신호: PSTAT_PRIOR_LSA_LIST_MAXED”

꼬리 블록은 임계값을 넘으면 가장 먼저, 무조건적으로 성능 카운터를 올린다:

// prior_lsa_next_record_internal -- src/transaction/log_append.cpp
if (log_Gl.prior_info.list_size >= (INT64) logpb_get_memsize ())
{
perfmon_inc_stat (thread_p, PSTAT_PRIOR_LSA_LIST_MAXED); /* <- maxed 이벤트마다 카운트 */
// ... 이후 모드별 분기 ...
}

PSTAT_PRIOR_LSA_LIST_MAXED프로듀서/컨슈머 불균형의 운영자 가시적 증상이다. 세 가지 모드 분기 중 어떤 것이 실행되기 전에 maxed 이벤트마다 한 번 증가하므로, 데몬 깨우기 경로와 자기 구제 소진 경로 모두를 계산한다. cubrid statdump에서 Num_prior_lsa_list_maxed가 상승한다면, 프로듀서가 소프트 상한선을 자주 넘을 만큼 flush 데몬을 앞서고 있다는 신호이므로 로그 장치 처리량, log_max_buffers 크기 설정, 또는 flush 데몬 스케줄링을 살펴볼 필요가 있다. 소진 측에서 logpb_prior_lsa_append_all_list(§9.4)가 내보내는 PSTAT_PRIOR_LSA_LIST_REMOVED / PSTAT_PRIOR_LSA_LIST_SIZE와 비교하면 실제로 얼마나 소진되었는지 알 수 있다.

logpb_get_memsize()(INT64) 캐스트를 적용하는 것이 중요하다: list_size는 부호 있는 INT64이므로 비교는 signed 대 signed여야 하며, size_t 반환값은 의도적으로 좁혀진다. 카운터를 올린 뒤, 블록은 두 개의 컴파일 타임 #if 분기와 SERVER_MODE 분기 내부의 런타임 분기 세 갈래로 나뉜다:

// prior_lsa_next_record_internal -- src/transaction/log_append.cpp
#if defined(SERVER_MODE)
if (!log_is_in_crash_recovery ()) /* <- SERVER_MODE 내부 런타임 분기 */
{
log_wakeup_log_flush_daemon (); /* (a) 데몬 깨우기 ... */
thread_sleep (1); /* ... 1ms 양보 */
}
else
{
LOG_CS_ENTER (thread_p); /* (b) 크래시 복구: 아직 데몬 없음 */
logpb_prior_lsa_append_all_list (thread_p); /* 직접 소진 */
LOG_CS_EXIT (thread_p);
}
#else
LOG_CS_ENTER (thread_p); /* (c) standalone: 데몬 없음 */
logpb_prior_lsa_append_all_list (thread_p);
LOG_CS_EXIT (thread_p);
#endif
stateDiagram-v2
  [*] --> Maxed: list_size >= memsize
  Maxed --> ServerMode: SERVER_MODE로 컴파일됨
  Maxed --> Standalone: SERVER_MODE 아님
  ServerMode --> Normal: 크래시 복구 중 아님
  ServerMode --> Recovery: 크래시 복구 중
  Normal --> Nudge: 데몬 깨우기 후 1ms sleep
  Recovery --> SelfHelp: LOG_CS 취득 후 append_all_list 후 LOG_CS_EXIT
  Standalone --> SelfHelp: LOG_CS 취득 후 append_all_list 후 LOG_CS_EXIT
  Nudge --> [*]
  SelfHelp --> [*]

Figure 9-2. 배압의 세 가지 결과. 두 가지는 컴파일 타임에 선택되고, SERVER_MODE 분기는 크래시 복구 상태에 따라 런타임에 한 번 더 갈라진다.

(a) SERVER_MODE, 정상 운영 — 깨우기와 양보. 이것이 스테디스테이트 프로덕션 경로다. 전용 flush 데몬(log_Flush_daemon)이 존재하므로 프로듀서는 로그 critical section에 전혀 관여하지 않는다. log_wakeup_log_flush_daemon을 호출하여 요청 플래그를 세우고 데몬을 깨운다:

// log_wakeup_log_flush_daemon -- src/transaction/log_manager.c
void
log_wakeup_log_flush_daemon ()
{
if (log_is_log_flush_daemon_available ()) /* <- log_Flush_daemon == NULL이면 false */
{
#if defined (SERVER_MODE)
log_Flush_has_been_requested = true; /* <- 가짜 깨우기 방지용 플래그 */
log_Flush_daemon->wakeup ();
#endif /* SERVER_MODE */
}
}

이후 프로듀서는 thread_sleep(1)을 호출하여 1밀리초 동안 양보한다. sleep이 실제 배압이다: 프로듀서 스레드가 자발적으로 멈춤으로써 데몬에게 CPU 시간을 주고, 디스크가 이 스레드가 더 많은 레코드를 enqueue하기 전에 따라잡을 기회를 준다. 강제가 아닌 협력적 방식이다 — 프로듀서가 스스로 sleep을 선택한 것이며 아무것도 계속 진행하는 것을 막지 않는다. log_wakeup_log_flush_daemon은 내부적으로 log_is_log_flush_daemon_available()로 보호되므로, 이 분기에서도 데몬이 없으면 깨우기는 no-op이 된다(프로듀서는 1ms sleep 후 다음 레코드에서 재확인) — 하지만 정상 SERVER_MODE에서는 데몬이 항상 존재한다.

(b) SERVER_MODE, 크래시 복구 중 — 자기 구제 소진. log_is_in_crash_recovery()는 재시작 복구 중, 즉 flush 데몬이 시작되기 전에 true를 반환한다. 깨울 데몬이 없으므로 깨우기-및-sleep 전략은 영구히 정체될 것이다 — 리스트가 절대 소진되지 않는다. 따라서 프로듀서가 인라인으로 소진한다: 로그 critical section에 진입(LOG_CS_ENTER)하고, logpb_prior_lsa_append_all_list를 호출하여 전체 리스트를 복사-분리(§9.4)하고, 빠져나온다. 이것이 “자기 구제” 경로다: 프로듀서가 한 소진 사이클 동안 컨슈머가 된다.

(c) Standalone(SERVER_MODE 아님) — 항상 자기 구제. Standalone/SA 빌드에는 flush 데몬이 없다(log_is_log_flush_daemon_available()이 하드코딩으로 false). #else 분기는 따라서 (b)와 동일하며 무조건적이다: 모든 maxed 이벤트가 LOG_CS 하에서 자기 구제 소진을 수행한다. log_is_in_crash_recovery() 검사가 없는 이유는 데몬이 없으면 복구 중이든 아니든 자기 구제가 유일한 선택이기 때문이다.

불변 조건 — prior_lsa_mutexLOG_CS를 취득하기 전에 해제되므로 자기 구제는 데드락이나 재귀를 일으킬 수 없다. Figure 9-1을 보면, 임계값 확인과 그에 따른 (a)/(b)/(c) 실행은 모두 log_Gl.prior_info.prior_lsa_mutex.unlock() 이후에 수행된다. 이것이 중요한 이유는 두 가지다. (1) 락 순서: logpb_prior_lsa_append_all_list는 내부적으로 LOG_CS를 보유한 상태에서 prior_lsa_mutex를 재취득한다(§9.4). 먼저 해제함으로써 프로듀서의 락 순서가 LOG_CSprior_lsa_mutex로 고정되어 소진 측과 일치한다; LOG_CS_ENTER를 넘어서 prior_lsa_mutex를 보유하면 그 재취득에서 자기 데드락이 발생한다. (2) 재귀 없음: logpb_prior_lsa_append_all_listprior_lsa_next_record_internal을 다시 호출하지 않으며, 프로듀서도 prior_lsa_mutex를 보유하지 않으므로 자기 구제 소진은 재귀가 아닌 평탄한 호출이다.

9.4 자기 구제가 재사용하는 소진 프리미티브: logpb_prior_lsa_append_all_list

섹션 제목: “9.4 자기 구제가 재사용하는 소진 프리미티브: logpb_prior_lsa_append_all_list”

두 자기 구제 분기 모두 flush 데몬이 소진에 사용하는 동일한 함수를 호출한다. 이 함수는 이 챕터와 챕터 6~7(분리 + 복사) 사이의 자연스러운 이음새다:

// 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;
INT64 current_size;
assert (LOG_CS_OWN_WRITE_MODE (thread_p)); /* <- 호출자는 반드시 쓰기 모드 LOG_CS 보유 */
log_Gl.prior_info.prior_lsa_mutex.lock (); /* <- §9.3 불변 조건이 보호하는 재취득 */
current_size = log_Gl.prior_info.list_size;
prior_list = prior_lsa_remove_prior_list (thread_p); /* 전체 리스트 분리, size 0으로 초기화 (Ch.6) */
log_Gl.prior_info.prior_lsa_mutex.unlock ();
if (prior_list != NULL) /* <- 빈 리스트 보호: 복사할 것이 없음 */
{
perfmon_add_stat (thread_p, PSTAT_PRIOR_LSA_LIST_SIZE, (unsigned int) current_size / ONE_K); /* KB 단위 소진량 */
perfmon_inc_stat (thread_p, PSTAT_PRIOR_LSA_LIST_REMOVED);
logpb_append_prior_lsa_list (thread_p, prior_list); /* 노드를 로그 페이지에 복사 (Ch.7) */
}
return NO_ERROR;
}

자기 구제 호출자 관점에서 이 함수를 분기별로 읽으면:

  1. assert (LOG_CS_OWN_WRITE_MODE) — 두 자기 구제 분기 모두 LOG_CS_ENTER/LOG_CS_EXIT으로 호출을 감쌌기 때문에 이 조건을 만족한다. 프로듀서가 직접 LOG_CS를 취득해야 하는 이유가 바로 이것이다; 이 함수는 스스로 취득하지 않는다.
  2. prior_lsa_mutex 하에서 스냅샷 + 분리. current_size를 읽고, prior_lsa_remove_prior_list가 전체 리스트를 분리한다(head/tail → NULL, list_size → 0; 챕터 6 참조). 뮤텍스는 동시 프로듀서로부터 분리를 보호하며 즉시 해제된다.
  3. prior_list != NULL 분기(비어있지 않은 경우): PSTAT_PRIOR_LSA_LIST_SIZE를 킬로바이트 단위로 기록하고 PSTAT_PRIOR_LSA_LIST_REMOVED를 올린 뒤, 분리된 체인을 챕터 7에서 모든 노드를 로그 페이지에 복사하는 logpb_append_prior_lsa_list에 넘긴다. 이 함수가 반환하면 list_size는 다시 0이 되고, 프로듀서의 다음 레코드 확인(§9.1)은 깨끗하게 통과한다.
  4. prior_list == NULL 분기(빈 경우): 다른 소진자(데몬 또는 경쟁한 자기 구제 프로듀서)가 이 프로듀서의 임계값 확인과 LOG_CS_ENTER 사이에 리스트를 이미 분리했다. 함수는 복사도 perf stat도 없이 조용히 NO_ERROR를 반환한다. 이것이 빈 리스트 보호가 존재하는 이유인 무해한 경쟁이다 — 경쟁에서 진 프로듀서는 불필요한 LOG_CS 작업을 했을 뿐 아무런 해를 끼치지 않는다.

불변 조건 — 성공적인 자기 구제 소진 후에는 list_size == 0이고 분리된 체인은 소진 스레드가 완전히 소유한다. prior_lsa_remove_prior_listprior_lsa_mutex 하에서 원자적으로 list_size를 0으로 초기화하고 head와 tail 포인터 모두를 null로 만든다. 따라서 동시 프로듀서는 절반만 분리된 리스트(head는 이동했지만 size는 아직 초기화 안 됨, 혹은 그 반대)를 절대 관찰할 수 없다. 반환된 prior_list는 이후 소진 스레드에 비공개이며 — 어떤 다른 스레드도 그 노드에 접근할 수 없으므로 — 이후의 logpb_append_prior_lsa_list 복사를 prior_lsa_mutex 밖에서(LOG_CS는 여전히 보유) 안전하게 실행할 수 있다. 원자성을 위반하면 — 예를 들어 head를 null로 만들기 전에 size를 초기화하면 — 프로듀서가 곧 복사되고 해제될 체인에 append할 수 있게 된다.

9.5 프로덕션에서는 깨우기-양보, 복구/standalone에서는 자기 구제인 이유

섹션 제목: “9.5 프로덕션에서는 깨우기-양보, 복구/standalone에서는 자기 구제인 이유”

이 비대칭성은 누가 소진할 수 있는가에서 비롯된다. 정상 SERVER_MODE에서는 flush 데몬이 지정된 소진자이므로 프로듀서는 LOG_CS에서 손을 떼고 순수하게 양보만으로 압력을 가한다 — 프로듀서도 자기 구제 소진을 수행하면 상한선을 넘을 만한 바로 그 부하 상황에서 데몬과 경합하게 된다. 크래시 복구 중에는 데몬이 아직 존재하지 않으며(복구는 백그라운드 서비스 시작 전에 로그를 재생), standalone에서는 데몬이 아예 없다; 전담자가 없으면 프로듀서가 소진하거나 리스트가 무한히 자라는 수밖에 없다. 단일 공유 프리미티브 logpb_prior_lsa_append_all_list는 복구, standalone, 그리고 데몬을 모두 동일한 복사-flush 로직으로 처리하며; 누가 호출하는지, 그리고 양보와 함께 감싸지는지의 여부만 다르다.

  1. **확인 조건은 list_size >= logpb_get_memsize()**이며, logpb_get_memsize()num_buffers * LOG_PAGESIZE — 인메모리 로그 페이지 버퍼의 바이트 용량이다. 양쪽 모두 바이트 단위이며, list_size에는 노드별 오버헤드가 포함되므로 의도적으로 약간 일찍 트리거된다.
  2. 상한선은 소프트다. 노드는 확인 전에 이미 연결되고 계산되었다; 임계값을 초과해도 프로듀서가 enqueue하는 것을 막지 않는다. 하드 흐름 제어는 이 블록이 아닌 데이터 페이지의 WAL 불변 조건에서 온다.
  3. PSTAT_PRIOR_LSA_LIST_MAXED는 maxed 이벤트마다 한 번, 모드 분기 이전에 증가한다 — 프로듀서가 flush 데몬을 앞서고 있다는 운영자 가시적 신호다.
  4. 세 가지 결과, 두 개는 컴파일 타임 하나는 런타임: (a) SERVER_MODE 정상 → log_wakeup_log_flush_daemon + thread_sleep(1) (깨우기와 양보, LOG_CS 미접촉); (b) SERVER_MODE 크래시 복구 중 → LOG_CS 하에서 자기 구제 소진; (c) standalone → 항상 LOG_CS 하에서 자기 구제 소진.
  5. 자기 구제가 존재하는 이유는 복구 중이나 standalone 빌드에서 flush 데몬이 없기 때문이다. 프로듀서가 일시적으로 컨슈머가 되어 데몬 자신의 소진 프리미티브 logpb_prior_lsa_append_all_list를 재사용한다.
  6. prior_lsa_mutexLOG_CS 취득 전에 해제되며, 이로써 락 순서가 LOG_CSprior_lsa_mutex로 고정되어 자기 구제 경로가 내부에서 prior_lsa_mutex를 재취득할 때 자기 데드락과 재귀 진입 모두를 방지한다.
  7. 소진은 경쟁에 관대하다: logpb_prior_lsa_append_all_listprior_lsa_mutex 하에서 원자적으로 분리하고 list_size를 0으로 초기화한 뒤, prior_list != NULL을 보호 조건으로 사용하므로 데몬에게 소진 경쟁에서 진 프로듀서는 아무 작업도 하지 않고 깨끗하게 반환한다.

Chapter 10: 특수 경로와 엣지 케이스

섹션 제목: “Chapter 10: 특수 경로와 엣지 케이스”

3~9장은 레코드 하나가 정상 상태의 생명주기를 거치는 경로를 추적했다 (뮤텍스 밖에서 빌드, LSN 할당 및 연결, 분리, LOG_PAGE 링으로 복사, 플러시, 배압(backpressure)). 이 장은 그 흐름에 맞지 않는 경로들을 모은다: 체크포인트의 세대를 넘나드는 뮤텍스 보유, TDE 플래그 전파, 노드 없는 LSA 스탬프, 셧다운 드레인 단언, 비어 있지 않은 리스트에서의 크래시, 그리고 비-커밋 강제 플러시(force-flush) 진입점 — 모두 하나의 규칙에서 파생된다:

마스터 인배리언트 (커밋 내구성). 트랜잭션은 자신의 LOG_COMMIT 레코드가 내구 상태가 되기 전까지 절대 커밋 ack를 받지 않는다. 아래의 모든 엣지 경로는 이 규칙을 직접 지키거나, 이 규칙과 무관한 노-옵(no-op)이다.

10.1 체크포인트 경로 — 레코드 생성 전 구간에서 뮤텍스 보유

섹션 제목: “10.1 체크포인트 경로 — 레코드 생성 전 구간에서 뮤텍스 보유”

일반적인 attach (prior_lsa_next_record, 5장)는 노드 하나를 처리할 때 내부적으로 prior_lsa_mutex를 잠근다. 체크포인트는 더 많은 것을 필요로 한다: LOG_END_CHKPT 페이로드(트랜잭션 테이블 + 시스템 top-op 배열)는 스탬프가 찍히는 동안 다른 어떤 레코드도 변경하지 않은 상태를 반영해야 한다 — 스냅샷과 attach 사이에 다른 워커의 LSN이 할당되면 기록된 redo LSA가 stale해진다. 따라서 logpb_checkpoint는 한 번 잠그고, 잠금을 유지한 채 레코드를 생성하고, with-lock 변형으로 attach한 뒤 잠금을 해제한다:

// logpb_checkpoint (END_CHKPT span) -- src/transaction/log_page_buffer.c (condensed)
log_Gl.prior_info.prior_lsa_mutex.lock (); /* <- held across the whole span */
// ... snapshot tran table + system topops, build node ...
node = prior_lsa_alloc_and_copy_data (thread_p, LOG_END_CHKPT, RV_NOT_DEFINED, NULL, ...);
if (node == NULL) {
log_Gl.prior_info.prior_lsa_mutex.unlock (); /* <- error path must release once */
goto error_cannot_chkpt;
}
// ... *chkpt = tmp_chkpt ...
prior_lsa_next_record_with_lock (thread_p, node, tdes); /* <- attach WITHOUT re-locking */
log_Gl.prior_info.prior_lsa_mutex.unlock (); /* <- single matching release */

prior_lsa_next_record_with_lockprior_lsa_next_record_internal (..., LOG_PRIOR_LSA_WITH_LOCK)을 감싼다. 5장에서 설명했듯이 내부 함수는 WITHOUT_LOCK 분기에서만 잠금을 획득한다; WITH_LOCK 분기는 호출자가 이미 잠금을 보유하고 있다고 가정하며 배압 꼬리(backpressure tail)도 건너뛴다 (9장의 list_size >= logpb_get_memsize() 자구책). 따라서 체크포인트는 unlock 이후 logpb_flush_pages_direct를 직접 호출하는 방식으로 내구성을 보장한다.

인배리언트 (단일 획득 — std::mutex는 비재귀적). 모든 _with_lock 호출자는 prior_lsa_mutex를 정확히 한 번 보유해야 하며, 오류 goto를 포함한 모든 경로에서 lock() 하나와 unlock() 하나를 쌍으로 맞춰야 한다. 이는 감사(audit)로만 강제된다: 내부 함수는 with-lock 분기에서 절대 스스로 잠그지 않는다. 위반 시 즉시 파괴됨 — 사전에 잠그고 평범한 prior_lsa_next_record(내부에서 재잠금)를 호출하는 호출자는 자기 자신을 데드락 상태로 만든다.

또 다른 with-lock 사용처는 복제-및-커밋 경로다: 오케스트레이터 log_append_repl_info_and_commit_log는 한 번 잠그고, log_append_repl_info_with_locklog_append_commit_log_with_lock(done-time 레코드인 log_append_donetime_internal도 포함)을 순서대로 호출한다. 이로써 외부 커밋이 한 트랜잭션의 복제 레코드와 LOG_COMMIT 사이에 끼어들지 못하도록 막는다 — 헤더 주석에도 그렇게 하면 “슬레이브/레플리카의 일관성이 깨진다”고 경고한다. 내부 log_append_repl_info_internalwith_lock 인자로 분기한다: LOG_PRIOR_LSA_WITH_LOCK 분기는 보유된 구간 안에서 prior_lsa_next_record_with_lock을 호출하고, 그 외에는 평범한 prior_lsa_next_record를 호출한다.

flowchart TD
  A["호출자: N개 레코드를\n인터리브 없이 기록해야 함"] --> B["prior_lsa_mutex.lock() 한 번"]
  B --> C["레코드 빌드\nprior_lsa_alloc_and_copy_*"]
  C --> D{"alloc 성공?"}
  D -->|아니오| E["unlock() 한 번\ngoto error"]
  D -->|예| F["prior_lsa_next_record_with_lock\n재잠금 없음, 배압 없음"]
  F --> G{"레코드 더 있음?"}
  G -->|예| C
  G -->|아니오| H["prior_lsa_mutex.unlock() 한 번"]
  H --> I["logpb_flush_pages_direct\n호출자 책임 내구성"]

Figure 10-1 — with-lock 원자 그룹 쓰기. 뮤텍스는 모든 레코드의 생성과 attach 구간을 포괄한다; D의 오류 경로에서 단일 잠금을 해제하고, 내구성(I)은 호출자의 고유 책임이다.

10.2 TDE — 노드에서 페이지로 tde_encrypted 전파

섹션 제목: “10.2 TDE — 노드에서 페이지로 tde_encrypted 전파”

prior 노드는 node->tde_encrypted라는 bool 하나를 갖는다. 프로듀서는 prior_set_tde_encrypted로 설정하고, 드레인 측에서는 prior_is_tde_encrypted로 읽는다:

// prior_set_tde_encrypted -- src/transaction/log_append.cpp
int
prior_set_tde_encrypted (log_prior_node *node, LOG_RCVINDEX recvindex)
{
if (!tde_is_loaded()) /* <- master key must be loaded */
{
er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_TDE_CIPHER_IS_NOT_LOADED, 0);
return ER_TDE_CIPHER_IS_NOT_LOADED; /* <- caller asserts(false) on this */
}
node->tde_encrypted = true; return NO_ERROR; /* <- success write */
}
// prior_is_tde_encrypted -- src/transaction/log_append.cpp
bool prior_is_tde_encrypted (const log_prior_node *node) { return node->tde_encrypted; } /* <- pure read */

세터는 두 가지 분기를 갖는다: cipher-not-loaded 오류(모든 호출자는 assert(false)로 처리한다 — 암호화되어야 할 레코드가 암호화될 수 없다면 이는 로직 오류다)와 성공 쓰기. public append API는 recvindex가 TDE 클래스 오브젝트를 가리킬 때만 이 함수를 호출한다. 드레인 측에서 logpb_append_next_record는 바이트를 복사하기 전에 플래그를 페이지 사이드 전역 변수로 읽어들이고, 복사 이후에 해제한다:

// logpb_append_next_record -- src/transaction/log_page_buffer.c
log_Gl.append.appending_page_tde_encrypted = prior_is_tde_encrypted (node); /* <- node flag -> page flag */
/* ... logpb_start_append, logpb_append_data x3, logpb_end_append ... */
log_Gl.append.appending_page_tde_encrypted = false; /* <- cleared after the record */

appending_page_tde_encrypted는 노드나 큐가 아닌 log_append_info (페이지 사이드 커서, 1장)의 멤버다. 이 값이 true인 동안, 두 소비자가 목적지 페이지에 브랜드를 찍는다:

// logpb_next_append_page -- src/transaction/log_page_buffer.c (page allocation)
if (log_Gl.append.appending_page_tde_encrypted) /* <- on a brand-new page: stamp unconditionally */
{
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); /* <- stamp the new page */
logpb_set_dirty (thread_p, log_Gl.append.log_pgptr);
}

logpb_start_append기존 페이지에 동일한 스탬프를 찍지만 멱등적으로 수행된다 — !LOG_IS_PAGE_TDE_ENCRYPTED (...)로 가드하여 logpb_next_append_page가 이미 브랜드를 찍지 않은 페이지에만 적용한다. 어느 쪽이든 브랜드는 쓰기 시점에 존중된다: 플러시 경로가 TDE 알고리즘 플래그를 가진 페이지를 암호화한다. 따라서 이 bool은 노드에서 드레인까지 전달되고, 일시적으로 페이지에 브랜드를 찍은 뒤 레코드 단위로 해제된다.

해저드 (빌드와 드레인 사이의 마스터 키 교체). 노드는 프로듀서 스레드에 로드된 마스터 키 세대 기준으로 플래그가 설정되지만, 실제 암호화는 드레인/쓰기 경로에서 나중에 일어난다 — 운영자가 그 사이에 키를 교체했을 수도 있다. 노드는 IV나 키 세대 정보를 전혀 보관하지 않고 bool만 갖고 있으므로, 이 창(window) 안에서 교체가 일어나면 의도는 이전 세대 기준으로 형성됐지만 페이지는 현재 세대로 브랜드된다. tde_is_loaded() 게이팅과 키 교체의 관리 작업적 성격이 유일한 완화책이다. 이는 companion 문서의 Open Question 6에 해당한다.

10.3 직접 LSA 스탬프-뮤텍스-하 경로

섹션 제목: “10.3 직접 LSA 스탬프-뮤텍스-하 경로”

log_skip_logging_set_lsa노드를 attach하지 않고 prior_lsa도 전진시키지 않는, 유일한 prior_lsa-뮤텍스 획득이다 — prior_lsa를 읽어 pgbuf_set_lsa로 데이터 페이지에 일관된 값을 스탬프하는 것이 전부다. 이는 데이터 페이지가 의도적으로 로깅되지 않는 경우 (예: log_no_logging 모드)에도 페이지 버퍼의 WAL 검사가 단조적으로 비교할 수 있는 LSA가 필요한 상황에 쓰인다:

// log_skip_logging_set_lsa -- src/transaction/log_manager.c
log_Gl.prior_info.prior_lsa_mutex.lock ();
(void) pgbuf_set_lsa (thread_p, addr->pgptr, &log_Gl.prior_info.prior_lsa); /* <- stamp current prior_lsa, do NOT advance it */
log_Gl.prior_info.prior_lsa_mutex.unlock ();

잠금 없이 읽으면 동시 attach가 prior_lsa를 전진시키는 동안 torn value를 관측할 수 있다; 뮤텍스는 페이지에 이후 append될 어떤 레코드보다도 <=인 일관된 LSA를 보장한다 — WAL 인배리언트가 정확히 요구하는 것이다 (페이지의 LSA는 내구 로그보다 앞서서는 안 된다). 레코드가 없으므로 커밋 ack도 없고, 따라서 마스터 인배리언트 관점에서는 노-옵이다.

log_prior_has_worker_log_records는 “prior 리스트에 아직 페이지로 드레인되지 않은 비시스템(사용자) 레코드가 남아 있는가?”를 묻는다. 유일한 호출자는 server_support.c의 서버 정지 시퀀스다: 워커와 vacuum을 멈추고 logpb_force_flush_pages가 실행된 뒤, 디버그 전용 assert (!log_prior_has_worker_log_records (thread_p))로 감싸져 로그 라이터가 멈추기 전에 워커 작업이 모두 드레인됐음을 확인한다 (HA와도 관련 있다 — 로그 라이터는 사용자 로그를 모두 먼저 ship해야 한다). 프로브는 잠금 하에서 live(미드레인) 리스트를 순회한다:

// log_prior_has_worker_log_records -- src/transaction/log_append.cpp (condensed)
LOG_CS_ENTER (thread_p); /* <- outer lock first */
std::unique_lock<std::mutex> ulock (log_Gl.prior_info.prior_lsa_mutex); /* <- inner lock second */
LOG_LSA nxio_lsa = log_Gl.append.get_nxio_lsa ();
if (!LSA_EQ (&nxio_lsa, &log_Gl.prior_info.prior_lsa)) /* <- durable cursor behind producer cursor? */
{
assert (LSA_LT (&nxio_lsa, &log_Gl.prior_info.prior_lsa)); /* <- nxio can only lag, never lead */
for (node = log_Gl.prior_info.prior_list_header; node != NULL; node = node->next)
if (node->log_header.trid != LOG_SYSTEM_TRANID) /* <- a USER record is pending */
{ ulock.unlock (); LOG_CS_EXIT (thread_p); return true; } /* <- early exit on first user node */
}
ulock.unlock (); LOG_CS_EXIT (thread_p);
return false; /* <- no user records pending */

분기 완전 읽기: nxio_lsa == prior_lsa이면 큐가 완전히 드레인되고 플러시된 상태 — 루프를 건너뛰고 return false. nxio_lsa < prior_lsa 이면(미드레인/미플러시 작업, assert가 커서는 뒤처질 수만 있음을 확인), 리스트를 순회한다: trid != LOG_SYSTEM_TRANID 노드(실제 워커 트랜잭션)를 만나면 즉시 true 반환 — 유일한 early-return; LOG_SYSTEM_TRANID 노드 (vacuum, 체크포인트 하우스키핑)는 건너뛴다; 시스템 노드만 남은 채 끝까지 도달하면 return false로 폴스루. 잠금 순서는 글로벌 규율과 일치한다 — LOG_CS (outer) before prior_lsa_mutex (inner) — 그리고 이것은 뮤텍스 하에서 live prior_list_header 체인을 순회하는 유일한 리더다; 드레인은 체인을 분리(detach)한 뒤 순회하므로(6장), 둘은 같은 리스트를 두고 결코 경쟁하지 않는다.

인배리언트 (커서 순서 — nxio_lsa <= prior_lsa). 내구 I/O 커서는 프로듀서 커서와 같거나 뒤처질 수 있으며, 절대 앞설 수 없다. 구조적으로 강제됨: prior_lsa는 attach 시에만(뮤텍스 하에서) 전진하고, nxio_lsa는 플러시 시에만(해당 바이트가 디스크에 기록된 후) 전진한다. 위반 시 파괴됨: 아직 생성하지 않은 LSN의 내구성을 주장하는 상황이 된다.

10.5 강제 플러시 래퍼와 WAL 인배리언트 소비자

섹션 제목: “10.5 강제 플러시 래퍼와 WAL 인배리언트 소비자”

두 개의 얇은 래퍼가 그룹 커밋 경로 밖에서 즉시 내구성이 필요한 호출자를 위해 존재한다. 둘 다 logpb_flush_pages_direct(드레인 + 쓰기, 7~8장)를 LOG_CS_ENTER/LOG_CS_EXIT 안에 감싼다. logpb_force_flush_pages는 순수한 “모든 것을 내구 상태로 만들기” 호출로, 서버 셧다운 (server_support.c), 힙/디스크/btree 벌크 연산, 페이지 버퍼, vacuum이 사용한다. 체크포인트는 이 래퍼를 사용하지 않는다 — END_CHKPT attach 이후(§10.1) logpb_flush_pages_direct를 직접 호출한다. logpb_force_flush_header_and_pages는 페이지 이후 logpb_flush_header 호출을 하나 추가한다: 헤더는 마지막 체크포인트와 end-of-log 위치를 기록하므로, 참조하는 페이지보다 먼저 쓰면 내구 데이터를 넘어서는 위치를 가리키게 된다. 페이지를 먼저, 헤더를 나중에.

WAL 인배리언트 소비자는 페이지 버퍼의 계약 집행자다:

// logpb_flush_log_for_wal -- src/transaction/log_page_buffer.c (condensed)
if (logpb_need_wal (lsa_ptr)) /* <- 1st check: data page ahead of durable log? */
{
LOG_CS_ENTER (thread_p);
if (logpb_need_wal (lsa_ptr)) /* <- 2nd check (double-checked under CS) */
logpb_flush_pages_direct (thread_p); /* <- force the log forward */
LOG_CS_EXIT (thread_p);
assert (LSA_ISNULL (lsa_ptr) || !logpb_need_wal (lsa_ptr)); /* <- WAL rule now holds */
}

페이지 버퍼 매니저는 더티 데이터 페이지를 쓰기 전에 이것을 호출한다. 이중 검사 방식으로, 세 가지 분기가 있다: (1) 외부 !logpb_need_wal — 내구 로그가 이미 lsa_ptr을 커버함, CS 없이 반환(핫 패스); (2) 외부 true, 내부 false — 창(window) 안에서 동시 플러시가 nxio_lsa를 페이지 LSA 너머로 전진시킨 것, 아무 것도 하지 않음; (3) 둘 다 true — 로그를 전진시키고, 사후 조건 assert로 페이지 쓰기가 안전함을 확인. 이곳에서 prior-list 드레인이 페이지 플러시를 대리하여 강제되며, 이로써 prior list는 커밋 경로뿐 아니라 페이지 내구성의 참여자가 된다.

10.6 비어 있지 않은 prior 리스트에서의 크래시

섹션 제목: “10.6 비어 있지 않은 prior 리스트에서의 크래시”

가장 중요한 엣지 케이스에는 코드가 가장 적다: 특별한 처리가 없으며, 그것이 올바른 설계다. 크래시 시점에 prior 리스트에는 로그 페이지에 한 번도 복사되지 않았고 당연히 fsync도 되지 않은, malloc된 LOG_PRIOR_NODE들이 임의 개수 남아 있을 수 있다. 이 노드들은 메모리 전용이며 결코 ack되지 않았다 — 커밋 중인 소유자는 여전히 logpb_flush_pagesgc_cond에서 nxio_lsa >= commit_lsa를 기다리며 파킹 중이므로, 어떤 클라이언트도 커밋됐다고 믿지 않는다. 복구는 LOG_COMMIT을 보지 못하고, 해당 트랜잭션을 크래시 시점 활성 상태로 표시하여 내구 레코드들을 깔끔하게 undo한다 (데이터 페이지는 WAL 인배리언트 §10.5를 준수함). 드레인되지 않은 노드들은 단순히 누수되고, OS가 프로세스 종료 시 힙을 회수한다. 오염시킬 풀도, 수리할 온-디스크 구조도 없다.

flowchart TD
  A["크래시, prior 리스트 비어 있지 않음"] --> B["노드 malloc됨,\n디스크 기록 없음, ack 없음"]
  B --> F["복구: LOG_COMMIT 없음\n-> 활성 표시 -> undo"]
  F --> G["WAL-안전 페이지 -> undo 롤백;\n누수 노드 -> OS가 종료 시 회수"]

Figure 10-3 — 비어 있지 않은 prior 리스트에서의 크래시. 내구성 게이트가 이미 유일하게 위험한 경우 — ack됐지만 내구 상태가 아닌 커밋 — 를 불가능하게 만들었으므로, 크래시 시점 정리 작업은 필요 없다.

  1. 체크포인트는 attach뿐 아니라 레코드 생성 구간에서도 prior_lsa_mutex를 보유한다, prior_lsa_next_record_with_lock으로. std::mutex는 비재귀적이다: 모든 with-lock 호출자는 한 번 잠그고, 오류 goto를 포함한 모든 경로에서 해제하며, 배압 자구책은 제공받지 못한다.
  2. TDE는 노드 위의 bool 하나다. prior_set_tde_encrypted (tde_is_loaded() 게이트)가 설정하고; 드레인이 prior_is_tde_encrypted를 읽어 appending_page_tde_encrypted로 옮기며, logpb_next_append_pagelogpb_start_append 전반에서 멱등적으로 페이지에 브랜드를 찍고, 레코드 단위로 해제한다. 그 창(window) 안에서의 마스터 키 교체는 비보호 상태 — 키 세대/IV 부기 없음 (Open Q 6).
  3. log_skip_logging_set_lsa는 레코드 없는 유일한 prior_lsa-뮤텍스 획득이다 — 일관된 prior_lsa를 읽어 pgbuf_set_lsa로 스탬프하고, 전진하거나 연결하지 않는다.
  4. log_prior_has_worker_log_records는 셧다운 시점 드레인 단언이다 (유일한 호출자: 서버 정지 시퀀스). nxio_lsa < prior_lsa일 때 첫 번째 비-LOG_SYSTEM_TRANID 노드에서 true를 반환한다.
  5. 강제 플러시 래퍼들과 logpb_flush_log_for_wal은 비-커밋 드레인 진입점이다. _force_flush_pages는 순수한 드레인+쓰기(체크포인트 아님); _header_and_pages는 페이지 이후에 헤더를 플러시한다; logpb_flush_log_for_wal은 이중 검사 후 더티 데이터 페이지가 home-write되기 전에 로그를 강제로 전진시킨다.
  6. 비어 있지 않은 리스트에서의 크래시는 처리가 필요 없다. 드레인되지 않은 노드들은 메모리 전용이며 종료 시 누수된다; 소유자는 결코 ack받지 않았으므로(마스터 인배리언트), 복구는 미커밋 작업을 올바르게 undo한다.

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

SymbolFileLine
heap_get_visible_version_from_logsrc/storage/heap_file.c25329
pgbuf_has_perm_pages_fixedsrc/storage/page_buffer.c11424
log_zip_undosrc/thread/thread_entry.hpp259
log_data_ptrsrc/thread/thread_entry.hpp261
log_Zip_supportsrc/transaction/log_append.cpp40
log_Zip_min_size_to_compresssrc/transaction/log_append.cpp41
LOG_PRIOR_LSA_LAST_APPEND_OFFSETsrc/transaction/log_append.cpp44
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.cpp129
LOG_RESET_PREV_LSAsrc/transaction/log_append.cpp137
LOG_APPEND_PTRsrc/transaction/log_append.cpp145
log_prior_has_worker_log_recordssrc/transaction/log_append.cpp152
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.cpp409
prior_lsa_copy_undo_data_to_nodesrc/transaction/log_append.cpp492
prior_lsa_copy_undo_data_to_nodesrc/transaction/log_append.cpp493
prior_lsa_copy_redo_data_to_nodesrc/transaction/log_append.cpp523
prior_lsa_copy_redo_data_to_nodesrc/transaction/log_append.cpp524
prior_lsa_copy_undo_crumbs_to_nodesrc/transaction/log_append.cpp554
prior_lsa_copy_redo_crumbs_to_nodesrc/transaction/log_append.cpp599
prior_lsa_gen_undoredo_record_from_crumbssrc/transaction/log_append.cpp650
prior_lsa_gen_undoredo_record_from_crumbssrc/transaction/log_append.cpp651
prior_lsa_gen_postpone_recordsrc/transaction/log_append.cpp1062
prior_lsa_gen_dbout_redo_recordsrc/transaction/log_append.cpp1109
prior_lsa_gen_2pc_prepare_recordsrc/transaction/log_append.cpp1144
prior_lsa_gen_end_chkpt_recordsrc/transaction/log_append.cpp1181
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
PSTAT_PRIOR_LSA_LIST_MAXEDsrc/transaction/log_append.cpp1524
thread_sleepsrc/transaction/log_append.cpp1531
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_append_realloc_data_ptrsrc/transaction/log_append.cpp1787
log_append_get_data_ptrsrc/transaction/log_append.cpp1858
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_DATA_ADDR_INITIALIZERsrc/transaction/log_append.hpp64
LOG_PRIOR_LSA_LOCKsrc/transaction/log_append.hpp66
LOG_PRIOR_LSA_WITHOUT_LOCKsrc/transaction/log_append.hpp68
LOG_PRIOR_LSA_WITH_LOCKsrc/transaction/log_append.hpp69
log_append_infosrc/transaction/log_append.hpp73
appending_page_tde_encryptedsrc/transaction/log_append.hpp81
log_prior_nodesrc/transaction/log_append.hpp91
log_prior_lsa_infosrc/transaction/log_append.hpp112
prior_flush_list_headersrc/transaction/log_append.hpp124
LOG_RV_RECORD_SET_MODIFY_MODEsrc/transaction/log_append.hpp204
log_zipsrc/transaction/log_compress.c45
log_diffsrc/transaction/log_compress.c176
MAKE_ZIP_LENsrc/transaction/log_compress.h33
log_zipsrc/transaction/log_compress.h53
LOG_IS_GROUP_COMMIT_ACTIVEsrc/transaction/log_impl.h124
log_Flush_has_been_requestedsrc/transaction/log_manager.c364
log_is_in_crash_recoverysrc/transaction/log_manager.c518
LOG_RESET_PREV_LSAsrc/transaction/log_manager.c887
log_initialize_internalsrc/transaction/log_manager.c1100
log_skip_logging_set_lsasrc/transaction/log_manager.c3225
log_append_repl_info_internalsrc/transaction/log_manager.c4555
log_append_repl_info_and_commit_logsrc/transaction/log_manager.c4647
log_append_donetime_internalsrc/transaction/log_manager.c4679
log_get_log_group_commit_intervalsrc/transaction/log_manager.c10044
log_wakeup_log_flush_daemonsrc/transaction/log_manager.c10126
log_is_log_flush_daemon_availablesrc/transaction/log_manager.c10141
log_flush_executesrc/transaction/log_manager.c10377
log_flush_daemon_initsrc/transaction/log_manager.c10493
LOG_CS_DEMOTEsrc/transaction/log_manager.c15246
LOG_CS_PROMOTEsrc/transaction/log_manager.c15257
LOG_CS_OWN_WRITE_MODEsrc/transaction/log_manager.h281
LOG_LAST_APPEND_PTRsrc/transaction/log_page_buffer.c162
LOG_APPEND_ALIGNsrc/transaction/log_page_buffer.c164
LOG_APPEND_ADVANCE_WHEN_DOESNOT_FITsrc/transaction/log_page_buffer.c177
LOG_APPEND_SETDIRTY_ADD_ALIGNsrc/transaction/log_page_buffer.c185
LOGPB_APPENDREC_STATUSsrc/transaction/log_page_buffer.c217
logpb_initialize_poolsrc/transaction/log_page_buffer.c553
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_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_end_appendsrc/transaction/log_page_buffer.c4455
logpb_checkpointsrc/transaction/log_page_buffer.c6877
logpb_get_memsizesrc/transaction/log_page_buffer.c11553
log_rectypesrc/transaction/log_record.hpp35
LOG_DIFF_UNDOREDO_DATAsrc/transaction/log_record.hpp122
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_dbout_redosrc/transaction/log_record.hpp254
log_rec_chkptsrc/transaction/log_record.hpp345
log_rec_2pc_prepcommitsrc/transaction/log_record.hpp388
LOG_IS_UNDOREDO_RECORD_TYPEsrc/transaction/log_record.hpp455
  • cubrid-prior-list.md — 상위 수준 동반 문서. 로그 append 파이프라인 전반을 다루는 cubrid-log-manager-detail.md도 참고할 것 (이 문서는 그 파이프라인의 프로듀서 측을 집중적으로 다룬다).
  • 코드: src/transaction/log_append.{cpp,hpp}, drain 측은 src/transaction/log_manager.c, 레코드 유형 정의는 src/transaction/log_record.hpp.
  • 방법론: knowledge/methodology/code-analysis-detail-doc.md.