CUBRID CDC (Change Data Capture) — 코드 수준 심층 분석
이 문서의 위치: 상위 수준 분석 문서인
cubrid-cdc.md는 설계 의도와 이론적 배경을 다룬다. 이 문서는 코드 수준에서 모든 분기와 필드를 추적한다. 각 챕터는 독립적으로 읽을 수 있지만, 순서대로 읽으면 캡처된 변경의 전체 생명주기 — 트랜잭션 로그 레코드에서 log reader를 거쳐 CDC 클라이언트에 전달되기까지 — 를 흐름 순서대로 따라갈 수 있다.
목차:
| Ch | 제목 | 상태 |
|---|---|---|
| 1 | 데이터 구조 지도 | ✅ |
| 2 | 프로듀서 측 보충 레코드 방출 | ✅ |
| 3 | 초기화와 프로듀서-컨슈머 데몬 쌍 | ✅ |
| 4 | 시작 LSA 설정과 유효성 검증 | ✅ |
| 5 | 프로듀서 데몬 외부 루프 | ✅ |
| 6 | 레코드별 디스패치와 단일 변경 재구성 | ✅ |
| 7 | 로우 이미지 구체화와 이벤트 페이로드 패킹 | ✅ |
| 8 | 클라이언트 API 생애주기와 와이어 디코딩 | ✅ |
| 9 | 엣지 경로 — 레거시 Applier와 공유 로그 리더 | ✅ |
Chapter 1: 데이터 구조 지도
섹션 제목: “Chapter 1: 데이터 구조 지도”이 챕터가 답하는 질문: CDC 상태를 담는 struct와 enum은 무엇이며, 서버 producer에서 클라이언트 log item까지 서로 어떻게 가리키는가? 상태는 struct를 공유하지 않는 두 주소 공간에 나뉘어 있으며, 네트워크로 통신한다. 서버(src/transaction/log_impl.h, CDC_GLOBAL 타입의 전역 cdc_Gl)와 libcubridcs 클라이언트(src/api/cubrid_log.h, CUBRID_LOG_ITEM 리스트)가 그것이다. Producer/consumer 모델 전반은 cubrid-cdc.md를 참조하라. SUPPLEMENT_REC_TYPE(2장의 입력)은 이름만 언급한다. 코드는 원문 그대로이며, 설명은 표에 담긴다.
1.1 서버 전경: cdc_Gl
섹션 제목: “1.1 서버 전경: cdc_Gl”extern CDC_GLOBAL cdc_Gl;은 클라이언트 연결 하나, 두 worker 컨텍스트, 그리고 그 사이의 lock-free 큐를 포함한다.
// cdc_global -- src/transaction/log_impl.htypedef struct cdc_global{ css_conn_entry conn; CDC_PRODUCER producer; CDC_CONSUMER consumer; /* *INDENT-OFF* */ lockfree::circular_queue<CDC_LOGINFO_ENTRY *> *loginfo_queue; /* *INDENT-ON* */ LOG_LSA first_loginfo_queue_lsa; LOG_LSA last_loginfo_queue_lsa; bool is_queue_reinitialized;} CDC_GLOBAL;큐는 포인터를 담는다. Producer가 힙에 할당해 enqueue하고, consumer가 dequeue해 해제한다.
| 필드 | 역할 / 이유 |
|---|---|
conn | css_conn_entry, CDC 클라이언트 하나; consumer가 이 연결로 데이터를 송신한다. |
producer | Producer 소유의 log-reader 컨텍스트 (1.2). |
consumer | Consumer 소유의 요청/응답 컨텍스트 (1.3). |
loginfo_queue | CDC_LOGINFO_ENTRY * SPSC 링에 대한 포인터; 단독으로 재초기화 가능. |
first_loginfo_queue_lsa | 버퍼에 남은 가장 오래된 항목의 LSA; “버퍼에 있는가?” 판별에 쓰인다. |
last_loginfo_queue_lsa | 가장 최근에 push된 항목의 LSA; producer의 high-water mark. |
is_queue_reinitialized | 큐 재구성 플래그; start_lsa로부터 재탐색을 시작한다. |
Invariant — LSA 윈도우가 살아있는 큐를 감싼다. 모든 항목의 next_lsa는 [first_loginfo_queue_lsa, last_loginfo_queue_lsa] 안에 있다. Producer는 push 시 상한을, consumer는 pop 시 하한을 각각 갱신한다. 이 조건이 깨지면 4장의 “버퍼에 있는가?” 판별이 오동작하며, is_queue_reinitialized가 탈출구가 된다.
flowchart LR
subgraph server["서버 — cdc_Gl (CDC_GLOBAL)"]
P["CDC_PRODUCER<br/>producer"]
C["CDC_CONSUMER<br/>consumer"]
Q["loginfo_queue<br/>circular_queue<CDC_LOGINFO_ENTRY*>"]
CONN["css_conn_entry<br/>conn"]
P -->|entry* push| Q
Q -->|entry* pop| C
C -->|직렬화 + 송신| CONN
end
subgraph client["클라이언트 — libcubridcs"]
LI["CUBRID_LOG_ITEM 리스트"]
end
CONN -.->|네트워크 wire| LI
Figure 1-1. Producer-큐-consumer-네트워크-클라이언트 전경. 큐는 포인터를, wire는 바이트를 운반한다.
1.2 CDC_PRODUCER — log-reader 컨텍스트
섹션 제목: “1.2 CDC_PRODUCER — log-reader 컨텍스트”// cdc_producer -- src/transaction/log_impl.htypedef struct cdc_producer{ LOG_LSA next_extraction_lsa; int all_in_cond; /* configuration */ int num_extraction_user; char **extraction_user; int num_extraction_class; UINT64 *extraction_classoids; volatile CDC_PRODUCER_STATE state; volatile CDC_PRODUCER_REQUEST request; int produced_queue_size; pthread_mutex_t lock; pthread_cond_t wait_cond; CDC_TEMP_LOGBUF temp_logbuf[2];/* *INDENT-OFF* */ std::unordered_map <TRANID, char *> tran_user; /*to clear when log producer ends suddenly */ std::unordered_map<TRANID, int > tran_ignore; /* *INDENT-ON* */} CDC_PRODUCER;| 필드 | 역할 / 이유 |
|---|---|
next_extraction_lsa | 읽기 커서; 다음 디코딩 대상 레코드 (5장). |
all_in_cond | 설정값: “cond”에 전체 old-image 포함 여부 — 키 컬럼뿐 아니라 모든 컬럼 (7장). |
num_extraction_user / extraction_user | 수량 + 이름 목록; 사용자 필터, 비어있으면 필터 없음. |
num_extraction_class / extraction_classoids | 수량 + class OID 배열; 테이블 필터, 비어있으면 전체. |
state | volatile CDC_PRODUCER_STATE; WAIT/RUN/DEAD 상태를 외부에 공개한다. |
request | volatile CDC_PRODUCER_REQUEST; consumer가 일시정지 또는 종료를 지시한다. |
produced_queue_size | 큐에 올라간 미처리 바이트 수; push 시 증가, consumed_queue_size로 주기적 감소; 역압(backpressure) 게이지 (1.10). |
lock | state/request 핸드오프를 보호하는 mutex. |
wait_cond | WAIT 슬립에 쓰이는 condvar; CPU를 소비하지 않는다. |
temp_logbuf[2] | 두 개의 scratch 버퍼 (1.5); 레코드가 페이지 경계를 걸칠 수 있다. |
tran_user | map<TRANID,char*> 커밋 사용자; 필터 및 user 필드, 비정상 종료 시 초기화. |
tran_ignore | map<TRANID,int> 무시 플래그; 필터된 트랜잭션을 저렴하게 건너뜀. |
Invariant — state는 현재 상태, request는 원하는 상태다. Producer는 state만, consumer는 request만 쓴다. 둘 다 volatile이다. 소유권이 없는 필드에 쓰면 교착상태가 발생한다 (producer가 STATE_WAIT인데 request가 RUN을 요구하는 상황; 3장). 유일한 중첩 struct는 temp_logbuf[2]이며, 맵의 키는 TRANID이다.
1.3 CDC_CONSUMER — 요청 처리 컨텍스트
섹션 제목: “1.3 CDC_CONSUMER — 요청 처리 컨텍스트”// cdc_consumer -- src/transaction/log_impl.htypedef struct cdc_consumer{ int extraction_timeout; int max_log_item; char *log_info; /* log info list. it is used as buffer to send to client */ int log_info_size; /* total length of data in log_info */ int log_info_buf_size; /* size of buffer for log_info */ int num_log_info; /* how many log info is stored in log_infos (log info list) */ int consumed_queue_size; volatile CDC_CONSUMER_REQUEST request; LOG_LSA start_lsa; /* first LSA of log info that should be sent */ LOG_LSA next_lsa; /* next LSA to be sent to client */} CDC_CONSUMER;| 필드 | 역할 / 이유 |
|---|---|
extraction_timeout | 채움 대기 시간(초); 만료 시 ..._EXTRACTION_TIMEOUT 반환. |
max_log_item | 한 번의 extract에서 반환할 항목 수 상한. |
log_info | 직렬화된 wire 바이트를 담는 힙 버퍼; 송신 직전까지의 staging 영역. |
log_info_size | 유효 바이트 수; 송신 길이, <= log_info_buf_size. |
log_info_buf_size | 할당된 용량; 오버플로우 시 확장. |
num_log_info | 패킹된 항목 수; max_log_item으로 제한. |
consumed_queue_size | 마지막 조정 이후 pop된 바이트 수; produced_queue_size에서 차감 후 0으로 초기화. |
request | volatile CDC_CONSUMER_REQUEST; RUN/WAIT/NONE. |
start_lsa | 요청된 첫 번째 LSA; 클라이언트 LSA에서 유래 (4장). |
next_lsa | 다음 요청에 쓸 LSA; 클라이언트 재개 커서. |
Invariant — 출력 삼중쌍은 자기 일관성을 유지한다. 0 <= log_info_size <= log_info_buf_size이며, num_log_info는 해당 바이트에 담긴 항목 수와 일치해야 한다. 7장은 크기와 카운트를 함께 증가시킨다. 크기를 초과하면 힙 오버플로우, 카운트와 바이트 불일치 시 8장의 디코더가 끝을 벗어나 읽게 된다.
Invariant — next_lsa는 호출 간 단조 증가(non-decreasing)한다. 각 extract는 반환된 모든 변경사항을 지나쳐 전진하며 다음 start_lsa의 씨앗이 된다. 역방향으로 움직이면 소비된 변경사항이 재전달된다. 유일한 예외: CUBRID_LOG_SUCCESS_WITH_ADJUSTED_LSA (4장)가 가장 가까운 유효 레코드로 앞으로 스냅한다.
1.4 CDC_LOGINFO_ENTRY — 큐에 담긴 변경 하나
섹션 제목: “1.4 CDC_LOGINFO_ENTRY — 큐에 담긴 변경 하나”// cdc_loginfo_entry -- src/transaction/log_impl.htypedef struct cdc_loginfo_entry{ LOG_LSA next_lsa; int length; char *log_info;} CDC_LOGINFO_ENTRY;| 필드 | 역할 / 이유 |
|---|---|
next_lsa | 이 항목 이후의 재개 LSA; CDC_CONSUMER::next_lsa를 설정한다. |
length | log_info의 바이트 길이; 복사 및 크기 회계에 쓰인다. |
log_info | 힙 blob, 직렬화된 변경 하나 (6-7장); consumer가 복사한다. |
Producer는 entry와 blob을 할당하고 포인터를 enqueue한다. Consumer는 blob을 CDC_CONSUMER::log_info에 복사하고, next_lsa를 취한 뒤 entry를 해제한다. Blob은 7장에서 구성되고 8장에서 디코딩된다.
1.5 CDC_TEMP_LOGBUF — 임시 로그 페이지
섹션 제목: “1.5 CDC_TEMP_LOGBUF — 임시 로그 페이지”// cdc_temp_logbuf -- src/transaction/log_impl.htypedef struct cdc_temp_logbuf{ LOG_PAGE *log_page_p; char log_page[IO_MAX_PAGE_SIZE + MAX_ALIGNMENT];} CDC_TEMP_LOGBUF;| 필드 | 역할 / 이유 |
|---|---|
log_page_p | CDC_GET_TEMP_LOGPAGE 매크로로 설정되는 LOG_PAGE * 뷰; 정렬된 핸들, 별도 할당 없음. |
log_page | 인라인 char[IO_MAX_PAGE_SIZE + MAX_ALIGNMENT]; 한 페이지 분량의 값 + 정렬 여유분. |
레코드가 페이지 경계를 걸칠 수 있기 때문에 두 개가 존재한다. CDC_GET_TEMP_LOGPAGE / CDC_CHECK_TEMP_LOGPAGE / CDC_UPDATE_TEMP_LOGPAGE 매크로는 temp_logbuf[(process_lsa)->pageid % 2]를 인덱스로 사용하며, 페이지 ID가 미스일 때만 logpb_fetch_page + memcpy를 수행한다. 버퍼는 홀짝(parity)으로 번갈아 쓰인다.
1.6 시그널링 enum
섹션 제목: “1.6 시그널링 enum”핸드셰이크를 구동한다. 상태 전이는 3장에서 다룬다.
// cdc_producer_state -- src/transaction/log_impl.htypedef enum cdc_producer_state{ CDC_PRODUCER_STATE_WAIT, CDC_PRODUCER_STATE_RUN, CDC_PRODUCER_STATE_DEAD } CDC_PRODUCER_STATE;
// cdc_consumer_request -- src/transaction/log_impl.htypedef enum cdc_consumer_request{ CDC_REQUEST_CONSUMER_TO_WAIT, CDC_REQUEST_CONSUMER_TO_RUN, CDC_REQUEST_CONSUMER_NONE } CDC_CONSUMER_REQUEST;
// cdc_producer_request -- src/transaction/log_impl.htypedef enum cdc_producer_request{ CDC_REQUEST_PRODUCER_TO_WAIT, CDC_REQUEST_PRODUCER_TO_BE_DEAD, CDC_REQUEST_PRODUCER_NONE } CDC_PRODUCER_REQUEST;| Enum | 값(순서대로)과 의미 |
|---|---|
CDC_PRODUCER_STATE | WAIT — wait_cond에서 대기 중 / RUN — 디코딩+push 중 / DEAD — 종료됨. |
CDC_CONSUMER_REQUEST | TO_WAIT — 유휴 / TO_RUN — 현재 요청 처리 / NONE — 대기 요청 없음. |
CDC_PRODUCER_REQUEST | TO_WAIT — 일시정지→STATE_WAIT / TO_BE_DEAD — 종료→STATE_DEAD / NONE — 대기 요청 없음. |
비대칭성: producer는 state(관찰)와 request(명령) 모두를 가지지만, consumer는 request만 가진다. Consumer의 진행 상황은 출력 삼중쌍과 큐의 LSA로 파악한다.
1.7 분류 enum
섹션 제목: “1.7 분류 enum”큐 항목이 어떤 종류의 변경인지를 태깅한다. 서버 측 디코딩 중 설정되고(6장), 클라이언트 측에서 재확인된다.
// cdc_dataitem_type -- src/transaction/log_impl.htypedef enum cdc_dataitem_type{ CDC_DDL = 0, CDC_DML, CDC_DCL, CDC_TIMER } CDC_DATAITEM_TYPE;
// cdc_dcl_type -- src/transaction/log_impl.htypedef enum cdc_dcl_type{ CDC_COMMIT = 0, CDC_ABORT } CDC_DCL_TYPE;
// cdc_dml_type -- src/transaction/log_impl.htypedef enum cdc_dml_type{ CDC_INSERT = 0, CDC_UPDATE, CDC_DELETE, CDC_TRIGGER_INSERT, CDC_TRIGGER_UPDATE, CDC_TRIGGER_DELETE } CDC_DML_TYPE;| Enum | 값과 의미 |
|---|---|
CDC_DATAITEM_TYPE | DDL 스키마 / DML 행 / DCL 트랜잭션 경계 / TIMER 하트비트. |
CDC_DCL_TYPE | COMMIT / ABORT — 어느 쪽 경계인가. |
CDC_DML_TYPE | INSERT/UPDATE/DELETE + 트리거에 의한 TRIGGER_* 변형. |
CDC_TIMER는 하트비트다. 해당 윈도우 내에 변경사항이 없을 경우, producer는 timer 항목을 emit해 재개 커서가 전진할 수 있게 한다. 이 정수들은 wire payload의 data_item_type 필드와 각 arm의 *_type 필드에 그대로 들어간다.
1.8 클라이언트 측: CUBRID_LOG_ITEM과 CUBRID_DATA_ITEM
섹션 제목: “1.8 클라이언트 측: CUBRID_LOG_ITEM과 CUBRID_DATA_ITEM”클라이언트는 이 자급자족 리스트만 본다. cubrid_log_extract가 head를 반환하고, 호출자는 next를 따라 걸으며 cubrid_log_clear_log_item으로 해제한다.
// cubrid_log_item -- src/api/cubrid_log.htypedef struct cubrid_log_item CUBRID_LOG_ITEM;struct cubrid_log_item{ int transaction_id; char *user; int data_item_type; CUBRID_DATA_ITEM data_item; CUBRID_LOG_ITEM *next;};
// cubrid_data_item -- src/api/cubrid_log.htypedef union cubrid_data_item CUBRID_DATA_ITEM;union cubrid_data_item{ DDL ddl; DML dml; DCL dcl; TIMER timer;};| 필드 | 역할 / 이유 |
|---|---|
transaction_id | 소유 트랜잭션 ID. |
user | 커밋 사용자 (힙 문자열); tran_user에서 유래. |
data_item_type | CDC_DATAITEM_TYPE 태그; 활성 arm을 선택한다. |
data_item | CUBRID_DATA_ITEM union; DDL/DML/DCL/TIMER 중 하나. |
next | 다음 항목 또는 NULL; 리스트를 구성한다. |
CUBRID_DATA_ITEM | 네 arm의 태그 없는 union; 태그는 외부에서 관리. |
Invariant — data_item_type이 union의 태그다. 태그가 CDC_DDL인데 data_item.dml을 읽는 것은 UB다. 디코더(8장)는 태그를 설정한 뒤 해당 arm을 채우며, cubrid_log_clear_log_item은 태그를 읽어 올바른 포인터를 해제한다. 불일치 시 가비지 읽기 또는 잘못된 해제가 발생한다.
1.9 클라이언트 payload 멤버 struct
섹션 제목: “1.9 클라이언트 payload 멤버 struct”union의 네 arm. 서버가 pack하고, 8장에서 디코딩한다.
// ddl -- src/api/cubrid_log.htypedef struct ddl DDL;struct ddl{ int ddl_type; int object_type; uint64_t oid; uint64_t classoid; char *statement; int statement_length;};
// dml -- src/api/cubrid_log.htypedef struct dml DML;struct dml{ int dml_type; uint64_t classoid; int num_changed_column; int *changed_column_index; char **changed_column_data; int *changed_column_data_len; int num_cond_column; int *cond_column_index; char **cond_column_data; int *cond_column_data_len;};
// dcl -- src/api/cubrid_log.htypedef struct dcl DCL;struct dcl{ int dcl_type; time_t timestamp;};
// timer -- src/api/cubrid_log.htypedef struct timer TIMER;struct timer{ time_t timestamp;};DDL 필드:
| 필드 | 역할 / 이유 |
|---|---|
ddl_type | 스키마 변경의 종류; DDL 세부 판별자. |
object_type | 대상 오브젝트 클래스. |
oid | 오브젝트의 OID. |
classoid | Class OID 컨텍스트. |
statement | 힙 SQL 문자열; 재현 가능, clear 시 해제. |
statement_length | 바이트 길이; 임베디드 바이트를 담을 수 있다. |
DML 필드 — 인덱스-병렬(index-parallel) 컬럼 배열 삼중쌍 두 쌍: changed (새 이미지)와 cond (이전/식별 이미지). all_in_cond (1.2)가 cond에 키 컬럼만 담을지 모든 컬럼을 담을지 결정한다.
| 필드 | 역할 / 이유 |
|---|---|
dml_type | CDC_DML_TYPE; insert/update/delete + 트리거 변형. |
classoid | 테이블의 class OID. |
num_changed_column | 변경 컬럼 수; changed 삼중쌍의 길이. |
changed_column_index | int* 새 이미지 컬럼의 위치 배열. |
changed_column_data | char** 새 값 blob 배열. |
changed_column_data_len | int* 새 값 바이트 길이 배열. |
num_cond_column | cond 컬럼 수; cond 삼중쌍의 길이. |
cond_column_index | int* 식별 컬럼의 위치 배열. |
cond_column_data | char** 이전/식별 값 blob 배열. |
cond_column_data_len | int* cond 값 바이트 길이 배열. |
DCL 및 TIMER 필드:
| Struct::필드 | 역할 / 이유 |
|---|---|
DCL::dcl_type | CDC_DCL_TYPE commit/abort 판별자. |
DCL::timestamp | time_t commit/abort 시각. |
TIMER::timestamp | time_t 하트비트 틱 (1.7); 클라이언트 커서를 전진시킨다. |
1.10 상한값과 큐 원소 타입
섹션 제목: “1.10 상한값과 큐 원소 타입”// MAX_CDC_LOGINFO_QUEUE -- src/transaction/log_impl.h#define MAX_CDC_LOGINFO_QUEUE_ENTRY 2048#define MAX_CDC_LOGINFO_QUEUE_SIZE 32 * 1024 * 1024 /*32 MB */| 매크로 | 값 | 역할 |
|---|---|---|
MAX_CDC_LOGINFO_QUEUE_ENTRY | 2048 | CDC_LOGINFO_ENTRY * 슬롯 상한; 링 용량. |
MAX_CDC_LOGINFO_QUEUE_SIZE | 32 MB | 바이트 상한; produced_queue_size(미처리 바이트)와 대조해 카운트와 무관하게 페이로드를 제한. |
항목 수는 대기 중인 변경사항의 개수를, 바이트는 사용 중인 메모리를 각각 제한하며, 둘 다 5장의 역압(backpressure) 로직에 쓰인다. 바이트 게이지는 누적 합계가 아니다. Producer는 주기적으로 produced_queue_size -= consumed_queue_size; consumed_queue_size = 0을 수행하므로, produced_queue_size는 큐에 아직 남아 있는 바이트를 반영한다. 큐 타입은 lockfree::circular_queue<CDC_LOGINFO_ENTRY *>로, 1.4의 할당/해제 소유권 구조에 맞는 포인터의 SPSC 링이다.
1.11 챕터 요약 — 핵심 정리
섹션 제목: “1.11 챕터 요약 — 핵심 정리”- 서버 전역 하나, 클라이언트 리스트 하나. 서버 상태는
cdc_Gl(CDC_GLOBAL,log_impl.h)에, 클라이언트 상태는CUBRID_LOG_ITEM리스트 (cubrid_log.h)에 있다. 공유 struct 없음 — 바이트는cdc_Gl.conn을 경유해 이동한다. - 큐는 값이 아닌 포인터를 운반한다.
loginfo_queue는lockfree::circular_queue<CDC_LOGINFO_ENTRY *>다. Producer가 할당해 포인터를 enqueue하고, consumer가 꺼내 복사한 뒤 해제한다. - state vs request가 핸드셰이크의 핵심이다.
CDC_PRODUCER::state는 현재 상태(producer가 씀),request는 원하는 상태(consumer가 씀)이며 둘 다volatile이다. Consumer는request만 가진다. - 두 LSA 윈도우가 일관성을 유지한다.
cdc_Gl.first/last_loginfo_queue_lsa가 살아있는 큐를 감싸고,CDC_CONSUMER::start_lsa/next_lsa가 진행 상황을 추적한다.next_lsa는ADJUSTED_LSA스냅을 제외하고 단조 증가한다. - 출력 삼중쌍은 자기 일관성을 유지한다.
log_info/log_info_size/log_info_buf_size+num_log_info는0 <= size <= buf_size이며 카운트와 바이트가 일치해야 한다. 그렇지 않으면 오버플로우 또는 디코더 이탈이 발생한다. - 양쪽에서 판별된 union을 사용한다.
CDC_DATAITEM_TYPE(+CDC_DCL_TYPE/CDC_DML_TYPE)이 서버 항목을 태깅하고, 클라이언트data_item_type이CUBRID_DATA_ITEMarm을 선택한다. 반드시 태그를 먼저 읽어야 한다. - 역압(backpressure)에는 두 가지 차원이 있다.
MAX_CDC_LOGINFO_QUEUE_ENTRY(2048)는 링을 카운트로,MAX_CDC_LOGINFO_QUEUE_SIZE(32 MB)는 바이트로 제한한다. 후자는produced_queue_size로 확인하며, producer가 주기적으로consumed_queue_size를 차감해 잔여 바이트를 반영한다.
Chapter 2: 프로듀서 측 보충 레코드 방출
섹션 제목: “Chapter 2: 프로듀서 측 보충 레코드 방출”독자 질문: CDC 컨슈머가 연결되기 전, 일반적인 DML 또는 DDL 트랜잭션은 어떻게 보충 레코드를 WAL에 기록하는가? 상위 수준 문서(cubrid-cdc.md의 “What CDC captures”와 “Supplemental logging” 절)는 CDC가 write-ahead log에 편승하는 이유를 다룬다. 이 챕터는 log_manager.c에 있는 네 가지 쓰기 측 appender와 이들이 디스크에 남기는 레코드 구조를 추적한다. 모든 동작은 작성자의 서버 트랜잭션 스레드 위에서 이루어지며, CDC 클라이언트는 아직 존재하지 않는다(컨슈머 측은 챕터 5–8에서 다룬다).
주석 규칙. 인라인
/* <- ... */주석은 편집자 설명이며, 나머지 주석 텍스트는 소스 그대로다.
2.1 하나의 로그 타입, 열한 개의 레코드 서브타입
섹션 제목: “2.1 하나의 로그 타입, 열한 개의 레코드 서브타입”CDC가 읽는 모든 보충 레코드는 마스터 log_rectype 열거형의 단일 WAL 로그 타입인 LOG_SUPPLEMENTAL_INFO다:
// LOG_SUPPLEMENTAL_INFO -- src/transaction/log_record.hpp LOG_SUPPLEMENTAL_INFO = 52, /* used for supplemental logs to support CDC interface. * it contains transaction user info, DDL statement, undo lsa, redo lsa for DML, * or undo images that never retrieved from the log. */페이로드의 종류는 WAL 로그 타입에 있지 않고, 한 단계 아래 레코드 자체 헤더의 SUPPLEMENT_REC_TYPE에 있다. 복구·플래시백·CDC 모두 LOG_SUPPLEMENTAL_INFO를 동일하게 디스패치하며(prior_lsa_gen_record가 data_header_length = sizeof (LOG_REC_SUPPLEMENT)로 설정), CDC 리더가 내부 rec_type을 분리해 해석한다.
// SUPPLEMENT_REC_TYPE -- src/transaction/log_record.hpptypedef enum supplement_rec_type{ LOG_SUPPLEMENT_TRAN_USER, LOG_SUPPLEMENT_UNDO_RECORD, /*Contains undo raw record that can not be retrieved from the logs */ LOG_SUPPLEMENT_DDL, /* Contains lsa of logs which contain undo, redo raw record (UPDATE, DELETE, INSERT) * | LOG_REC_HEADER | SUPPLEMENT_REC_TYPE | LENGTH | CLASS OID | UNDO LSA (sizeof LOG_LSA) | REDO LSA | */ LOG_SUPPLEMENT_INSERT, LOG_SUPPLEMENT_UPDATE, LOG_SUPPLEMENT_DELETE, LOG_SUPPLEMENT_TRIGGER_INSERT, /* INSERT, UPDATE, DELETE logs appended by a trigger action */ LOG_SUPPLEMENT_TRIGGER_UPDATE, LOG_SUPPLEMENT_TRIGGER_DELETE, LOG_SUPPLEMENT_LARGER_REC_TYPE,} SUPPLEMENT_REC_TYPE;열한 개 값 모두 단일 방출자 테이블로 대응된다:
| 값 | 방출 위치 | 본문 구조 |
|---|---|---|
LOG_SUPPLEMENT_TRAN_USER | 커밋 / DDL 사이트 | db_user 문자열; 트랜잭션당 1회 (2.7) |
LOG_SUPPLEMENT_UNDO_RECORD | ..._undo_record | recdes.type + 리더가 재조회 불가한 원시 바이트 (2.5) |
LOG_SUPPLEMENT_DDL | ..._serial; 네트워크 사이트 | packed int + OID + 인라인 SQL (2.6) |
LOG_SUPPLEMENT_INSERT | ..._lsa | OID + REDO LSA, 이전 이미지 없음 (2.4) |
LOG_SUPPLEMENT_UPDATE | ..._lsa | OID + UNDO + REDO LSA, 양쪽 이미지 |
LOG_SUPPLEMENT_DELETE | ..._lsa | OID + UNDO LSA, 새 이미지 없음 |
LOG_SUPPLEMENT_TRIGGER_INSERT/UPDATE/DELETE | DML, trigger_involved 설정 시 | DML 삼중쌍과 동일 |
LOG_SUPPLEMENT_LARGER_REC_TYPE | 방출 없음 | 범위 sentinel; 반드시 마지막에 위치 |
LOG_SUPPLEMENT_DDL 뒤의 인라인 주석은 DML 그룹과 TRIGGER_* 쌍이 공유하는 본문 레이아웃(LSA, 이미지 아님, 2.4)을 기술한다.
불변 조건 — 내부 rec_type이 유일한 판별자다. 열한 개 서브타입 모두 하나의 WAL 로그 타입 안에 실린다. 외부 LOG_RECORD_HEADER는 이들을 구별하지 않는다. LOG_SUPPLEMENT_LARGER_REC_TYPE 앞에 서브타입을 삽입하면 rec_type < LOG_SUPPLEMENT_LARGER_REC_TYPE 범위 검사가 묵시적으로 확장되므로, sentinel은 반드시 마지막에 위치해야 한다.
2.2 보충 레코드 헤더
섹션 제목: “2.2 보충 레코드 헤더”LOG_SUPPLEMENTAL_INFO 본문은 prior_lsa_gen_record가 data_header로 예약하는 고정 2필드 헤더로 시작한다:
// log_rec_supplement -- src/transaction/log_record.hpptypedef struct log_rec_supplement LOG_REC_SUPPLEMENT;struct log_rec_supplement{ SUPPLEMENT_REC_TYPE rec_type; int length;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
rec_type | 이후 열한 개 페이로드 중 어느 것인지 지정 | 리더의 디코드 switch에서 유일한 판별자 (2.1) |
length | 헤더 이후 페이로드의 바이트 길이 | 리더가 정확히 skip/copy 가능하게 함; 최상위 비트는 압축 플래그로 이중 사용 |
페이로드는 이 struct 안에 없으며, 이전 노드의 undo data로 실린다(2.3). length는 오버로드된다: 압축된 페이로드는 MAKE_ZIP_LEN(length) ((length) | 0x80000000)를 기록하고, log_compress.h의 ZIP_CHECK / GET_ZIP_LEN이 이를 읽어 복원한다.
불변 조건 — length의 최상위 비트는 “압축됨”을 의미하며 “음수”가 아니다. 리더는 length를 원시 크기로 처리하기 전에 반드시 ZIP_CHECK를 수행해야 한다. 그렇지 않으면 압축 레코드를 약 2 GB 과다 읽게 된다. 2.3이 압축 분기에서만 MAKE_ZIP_LEN(...)을 저장하는 이유가 바로 이 때문이다.
디스크 상 중첩 구조는 세 층이다: 외부 LOG_RECORD_HEADER (type = LOG_SUPPLEMENTAL_INFO), 내부 LOG_REC_SUPPLEMENT, 그리고 이전 노드의 undo data로 실리는 페이로드.
2.3 log_append_supplemental_info — 범용 appender
섹션 제목: “2.3 log_append_supplemental_info — 범용 appender”모든 보충 레코드는 여기서 생성된다. 보충 로깅에서 prior_lsa_alloc_and_copy_data를 호출하는 유일한 지점이며, 2.4–2.6의 헬퍼들은 이 함수 위에 얹힌 얇은 포매터다. 핵심 라인들:
// log_append_supplemental_info -- src/transaction/log_manager.c assert (prm_get_integer_value (PRM_ID_SUPPLEMENTAL_LOG) > 0); if (length >= log_Zip_min_size_to_compress && log_Zip_support) /* branch A */ { zip_undo = log_append_get_zip_undo (thread_p); if (zip_undo == NULL) { return; /* <- no zip buffer: record silently dropped */ } log_zip (zip_undo, length, data); // ... rebind length = zip_undo->data_length, data = zip_undo->log_data; is_zipped = true ... } /* supplement data will be stored at undo data */ node = prior_lsa_alloc_and_copy_data (thread_p, LOG_SUPPLEMENTAL_INFO, RV_NOT_DEFINED, NULL, length, (char *) data, 0, NULL); if (node == NULL) { return; /* <- allocation failed: record dropped */ } supplement = (LOG_REC_SUPPLEMENT *) node->data_header; supplement->rec_type = rec_type; supplement->length = is_zipped ? MAKE_ZIP_LEN (zip_undo->data_length) : length; /* <- src uses if/else */ prior_lsa_next_record (thread_p, node, tdes);Figure 2-1은 모든 분기를 담는다. 두 가지 비직관적인 경우는 묵시적 드롭이다 — zip 버퍼가 NULL이거나 node가 NULL일 때 모두 에러 없이 return한다. 진입 assert(> 0)는 릴리스 빌드에서 컴파일 아웃되므로, 호출자가 2.8의 방식으로 게이트를 담당한다.
불변 조건 — 보충 레코드는 복구에 영향을 주지 않는다. RV_NOT_DEFINED로 인해 크래시 복구가 이를 건너뛴다. 보충 레코드는 재실행되지 않고 CDC/플래시백이 순방향으로 읽기만 하므로, 정확성은 LSA 순서의 존재 여부에만 의존한다.
flowchart TD
S["호출자 (PRM_ID_SUPPLEMENTAL_LOG로 게이트됨)"] --> Z{"length >= 255<br/>and log_Zip_support?"}
Z -- yes --> ZB{"zip buffer?"}
ZB -- no --> R1["return: dropped"]
ZB -- yes --> ZC["log_zip; rebind; is_zipped=true"]
Z -- no --> AL
ZC --> AL["prior_lsa_alloc_and_copy_data(LOG_SUPPLEMENTAL_INFO)"]
AL --> NN{"node == NULL?"}
NN -- yes --> R2["return: dropped"]
NN -- no --> HF["rec_type; length 설정"]
HF --> PUB["prior_lsa_next_record"]
Figure 2-1. 두 개의 묵시적 드롭 early return을 포함한 모든 분기.
2.4 DML 포매터 — log_append_supplemental_lsa
섹션 제목: “2.4 DML 포매터 — log_append_supplemental_lsa”INSERT/UPDATE/DELETE 및 트리거 변형의 핵심 처리기다. 이 함수의 결정적 특성은 간접 참조다: 행 이미지 자체가 아닌 기존 WAL undo/redo 레코드를 가리키는 LSA를 기록한다.
// log_append_supplemental_lsa -- src/transaction/log_manager.c int size; /* sizeof (OID) = 8, sizeof (LOG_LSA) = 8; raw memcpy, not OR_PUT_*, so OR_OID_SIZE/OR_LOG_LSA_SIZE unused */ char data[24]; assert (prm_get_integer_value (PRM_ID_SUPPLEMENTAL_LOG) > 0); switch (rec_type) { case LOG_SUPPLEMENT_INSERT: case LOG_SUPPLEMENT_TRIGGER_INSERT: assert (redo_lsa != NULL); size = sizeof (OID) + sizeof (LOG_LSA); /* <- OID + redo only */ // ... memcpy classoid, redo_lsa ... break; case LOG_SUPPLEMENT_UPDATE: case LOG_SUPPLEMENT_TRIGGER_UPDATE: assert (undo_lsa != NULL && redo_lsa != NULL); size = sizeof (OID) + sizeof (LOG_LSA) + sizeof (LOG_LSA); /* <- OID + undo + redo */ // ... memcpy classoid, undo_lsa, redo_lsa ... break; case LOG_SUPPLEMENT_DELETE: case LOG_SUPPLEMENT_TRIGGER_DELETE: assert (undo_lsa != NULL); size = sizeof (OID) + sizeof (LOG_LSA); /* <- OID + undo only */ // ... memcpy classoid, undo_lsa ... break; default: assert (false); return ER_FAILED; /* <- non-DML rec_type */ } log_append_supplemental_info (thread_p, rec_type, size, (void *) data); return NO_ERROR;세 개의 유효 case 그룹과 default 에러 경로가 있다. INSERT는 OID + redo(size = 16)를 패킹하고, UPDATE는 OID + undo + redo(size = 24, data[24] 전부 채움)를, DELETE는 OID + undo(size = 16)를 패킹한다. 각 TRIGGER_* case는 바이트 단위로 일반 twin과 동일하며, 오직 rec_type 태그만 다르다. default는 assert(false)를 트리거하고 ER_FAILED를 반환하며 아무것도 방출하지 않는다(이 함수는 계약상 DML 전용이다). heap_file.c의 세 호출 지점 각각이 thread_p->trigger_involved ? LOG_SUPPLEMENT_TRIGGER_* : LOG_SUPPLEMENT_*로 태그를 선택한다. 세 방출 사이트의 게이트 조건은 균일하지 않다: heap_insert_logical은 !LSA_ISNULL (&context->supp_redo_lsa) && context->recdes_p->type != REC_ASSIGN_ADDRESS 조건을 do_supplemental_log 검사에 추가한다 — 주소만 있는 placeholder는 실제 이미지 LSA가 없어 건너뛴다 — 반면 heap_delete_logical과 heap_update_logical은 context->do_supplemental_log만으로 게이트하며, 이미 supp_undo_lsa(update의 경우 supp_redo_lsa도)가 tail LSA로 채워진 상태다.
불변 조건 — DML 보충 레코드는 이미지가 아닌 LSA를 저장한다. 페이로드는 행 너비에 관계없이 최대 24바이트다. CDC는 undo/redo LSA를 따라 이미 기록된 WAL 레코드로 이동해 나중에 이미지를 구체화한다(챕터 7). 그 대가는 디코드 시 두 번째 WAL 읽기와, 참조된 레코드의 재접근 가능성 요건이다 — 이것이 다음 서브타입의 존재 이유다.
2.5 log_append_supplemental_undo_record — 이미지를 재조회할 수 없는 경우
섹션 제목: “2.5 log_append_supplemental_undo_record — 이미지를 재조회할 수 없는 경우”일부 undo 이미지는 overflow 또는 forwarded heap 레코드에 위치하므로 CDC 리더가 LSA를 따라 재구성할 수 없다(chain이 재편되었을 수 있다). 이 경우 프로듀서가 원시 바이트를 인라인으로 포함시킨다:
// log_append_supplemental_undo_record -- src/transaction/log_manager.c int length = undo_recdes->length + sizeof (undo_recdes->type); char *data = (char *) malloc (length); if (data == NULL) { er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_OUT_OF_VIRTUAL_MEMORY, 1, length); return ER_OUT_OF_VIRTUAL_MEMORY; /* <- only error path */ } memcpy (data, &undo_recdes->type, sizeof (undo_recdes->type)); /* <- prepend the record type */ memcpy (data + sizeof (undo_recdes->type), undo_recdes->data, undo_recdes->length); log_append_supplemental_info (thread_p, LOG_SUPPLEMENT_UNDO_RECORD, length, data); free_and_init (data); return NO_ERROR;두 개의 분기가 있다. malloc 실패는 유일한 에러 경로다. 성공 경로는 2바이트 recdes.type(REC_HOME, REC_BIGONE 등을 컨슈머가 알 수 있도록)을 앞에 prepend한 뒤 범용 appender에 전달하고, 버퍼를 해제한다. 페이로드가 클 수 있으므로 이 서브타입은 가장 자주 zip 분기(2.3)를 타게 된다.
2.6 log_append_supplemental_serial — 시리얼을 위한 DDL 형식 레코드
섹션 제목: “2.6 log_append_supplemental_serial — 시리얼을 위한 DDL 형식 레코드”시리얼 next-value 소비는 합성된 SQL 문을 담는 LOG_SUPPLEMENT_DDL 레코드로 기록된다. 컨슈머는 카탈로그 변경을 추적하는 대신 이 문장을 재실행해 advance를 재현할 수 있다:
// log_append_supplemental_serial -- src/transaction/log_manager.c int ddl_type = 1; /* <- 1 = DDL */ int obj_type = 2; /* <- 2 = serial */ if (cached_num == 0) { cached_num = 1; /* <- normalize so the generated SQL is well-formed */ } sprintf (stmt, "SELECT SERIAL_NEXT_VALUE(%s, %d);", serial_name, cached_num); // ... size data_len; malloc (+MAX_ALIGNMENT); on fail er_set ER_OUT_OF_VIRTUAL_MEMORY, return (only error) ... ptr = start_ptr = supplemental_data; // ... or_pack_int ddl_type, obj_type; or_pack_oid classoid, serial_oid; or_pack_int strlen; or_pack_string stmt ... data_len = ptr - start_ptr; /* <- recompute: packing may pad to alignment */ log_append_supplemental_info (thread_p, LOG_SUPPLEMENT_DDL, data_len, (void *) supplemental_data);두 개의 분기가 있다: cached_num == 0 정규화와 malloc 실패(유일한 에러 반환). DML 포매터의 raw memcpy와 달리, DDL 본문은 가변 길이 문자열을 위해 or_pack 인코딩(ddl_type | obj_type | class OID | serial OID | stmt len | stmt text)으로 기록되며, data_len은 패킹 후 커서 위치에서 재계산된다. 이 레이아웃은 실제 DDL 문 사이트(2.7)에서 그대로 반영된다.
2.7 DDL 방출 사이트와 두 개의 TRAN_USER 사이트
섹션 제목: “2.7 DDL 방출 사이트와 두 개의 TRAN_USER 사이트”전체 문장 DDL 레코드는 log_manager.c가 아닌 network_interface_sr.cpp의 네트워크 핸들러에서 방출된다. 형식은 2.6과 동일하며, 이곳이 두 개의 LOG_SUPPLEMENT_TRAN_USER 사이트 중 첫 번째다:
// (DDL statement supplemental emit) -- src/communication/network_interface_sr.cpp if (prm_get_integer_value (PRM_ID_SUPPLEMENTAL_LOG) == 1) { // ... unpack ddl_type, obj_type, classoid, oid, stmt_text; or_pack into supplemental_data ... tdes = LOG_FIND_CURRENT_TDES (thread_p); if (!tdes->has_supplemental_log) { log_append_supplemental_info (thread_p, LOG_SUPPLEMENT_TRAN_USER, /* ... db_user ... */); tdes->has_supplemental_log = true; /* <- first supplemental record in this txn */ } log_append_supplemental_info (thread_p, LOG_SUPPLEMENT_DDL, data_len, supplemental_data); }트랜잭션에서 첫 번째 보충 활동이 DDL인 경우, 사용자를 즉시(eagerly) 기록하고 has_supplemental_log를 뒤집는다. 두 번째 사이트는 커밋 시점이며, 보충 활동이 DML뿐이었던 트랜잭션을 위해 존재한다(동일한 블록이 log_append_repl_info_and_commit_log — prior_lsa_mutex 한 번 보유 하에 repl + commit을 함께 쓰는 HA 변형 — 에도 나타난다):
// log_append_commit_log -- src/transaction/log_manager.c if (tdes->has_supplemental_log) { log_append_supplemental_info (thread_p, LOG_SUPPLEMENT_TRAN_USER, /* ... db_user ... */); tdes->has_supplemental_log = false; /* <- consume the flag at commit */ } // ... log_append_donetime_internal (... LOG_COMMIT ...) writes the commit record ...사용자 이름이 행 이벤트와 분리된 이유. DML 행 이벤트(2.4)는 행별 사용자 귀속 정보를 담지 않는다. db_user는 트랜잭션 수준의 사실이며 트랜잭션당 한 번 방출되고, 컨슈머가 동일 트랜잭션 윈도우 안의 모든 행 이벤트에 이를 연결한다(챕터 6).
불변 조건 — 트랜잭션당 최대 하나의 LOG_SUPPLEMENT_TRAN_USER. has_supplemental_log가 게이트 역할을 한다(Figure 2-2가 수명 주기를 추적). 방출기가 두 개 이상이면 이중 계산 또는 잘못된 귀속이 발생한다. 이 플래그는 abort 시(log_append_abort_log)에도 클리어되므로, 롤백된 트랜잭션은 미결 기대를 남기지 않는다.
stateDiagram-v2 [*] --> NoSupp: 트랜잭션 시작, 플래그 false NoSupp --> UserLogged: 첫 DDL -> TRAN_USER 방출, 플래그 설정 NoSupp --> DmlPending: 첫 DML -> 플래그 설정, 사용자 미기록 DmlPending --> DmlPending: 추가 DML, 플래그 유지 UserLogged --> UserLogged: 추가 DDL 또는 DML, 플래그 유지 DmlPending --> Committed: 커밋 -> TRAN_USER 방출, 플래그 클리어 UserLogged --> Committed: 커밋, DDL 사이트에서 이미 클리어 DmlPending --> Aborted: abort -> 플래그 클리어, 사용자 레코드 없음 Committed --> [*] Aborted --> [*]
Figure 2-2. 트랜잭션당 사용자 레코드 하나를 보장하는 has_supplemental_log 상태 머신.
2.8 opt-in 게이트
섹션 제목: “2.8 opt-in 게이트”게이트 PRM_ID_SUPPLEMENTAL_LOG는 네트워크 DDL 사이트(2.7의 == 1 테스트)에서 한 번 더 검사된다. 한 번만 신뢰하지 않는 이유는 해당 소스 주석이 설명한다: 브로커(CAS)와 서버가 독립적인 파라미터 복사본을 가질 수 있으며, 브로커가 파라미터 값이 변경된 상태로 재시작될 수 있기 때문이다.
보충 로깅은 간헐적인 zip에 따른 WAL 볼륨 증가와 CPU 부담을 수반하므로, 기본적으로 비활성화되어 있다. 비활성화 상태에서 트랜잭션은 보충 레코드를 전혀 기록하지 않으며, CDC에는 보이지 않는다 — 챕터 4가 의존하는 계약이다.
2.9 챕터 요약 — 핵심 정리
섹션 제목: “2.9 챕터 요약 — 핵심 정리”- WAL 타입 하나, 서브타입 열하나. CDC가 읽는 모든 레코드는
LOG_SUPPLEMENTAL_INFO(52)이며, 판별자는 내부LOG_REC_SUPPLEMENT.rec_type이고LOG_SUPPLEMENT_LARGER_REC_TYPE이 범위 sentinel로 마지막에 위치한다. log_append_supplemental_info가 단일 병목이다. 압축(MAKE_ZIP_LEN/length최상위 비트),RV_NOT_DEFINED할당(복구 비대상), 두 개의 묵시적 드롭 early return을 모두 이곳이 관장한다.- DML 레코드는 이미지가 아닌 LSA를 담는다.
log_append_supplemental_lsa는 INSERT/UPDATE/DELETE(TRIGGER_*포함) switch에 따라 OID + undo/redo LSA를 16–24바이트로 패킹하며, 비용은 행 너비와 무관하다. - 두 가지 본문 인코딩. DML은 raw
memcpy를 사용하고, DDL/시리얼은 가변 길이 SQL을 위해or_pack을 사용한다.recdes인라인 undo 레코드 경로는 LSA로 재조회할 수 없는 이미지를 위해 존재한다. - 트랜잭션당 사용자 레코드 하나.
LOG_SUPPLEMENT_TRAN_USER는 첫 번째 DDL 사이트에서 즉시, 또는 커밋 시 지연 방출되며,has_supplemental_log로 게이트된다. - 프로듀서 측 전체가 opt-in이다.
PRM_ID_SUPPLEMENTAL_LOG가 모든 방출 경로를 게이트하며, 브로커와 서버가 불일치할 수 있으므로 네트워크 사이트에서 재검사한다.
Chapter 3: 초기화와 프로듀서-컨슈머 데몬 쌍
섹션 제목: “Chapter 3: 초기화와 프로듀서-컨슈머 데몬 쌍”이 챕터는 하나의 질문에 답한다: CDC 컨슈머가 접속할 때 서브시스템은 어떻게 부트스트랩되며, 어떤 스레드와 메모리가 생성되는가? 구조체(CDC_GLOBAL, CDC_PRODUCER, CDC_CONSUMER, lock-free loginfo_queue, temp_logbuf[2])는 Chapter 1에서 필드 단위로 다루었다. 여기서는 이들을 생성하고 해제하는 코드, 그리고 문서 전반에서 참조되는 시그널링 프리미티브를 추적한다. CDC의 설계 의도는 companion 문서 cubrid-cdc.md의 “Producer/consumer split”, “Log info queue” 절을 참고하라.
두 개의 생명주기는 별개로 유지된다. Boot — cdc_daemons_init / cdc_daemons_destroy는 src/transaction/boot_sr.c의 서버 시작/종료 시 한 번 실행되며, 단일 프로듀서 데몬과 전역 메모리를 생성한다. Session — cdc_set_configuration, cdc_reinitialize_queue, cdc_cleanup은 각 attach/re-seek/detach 시 network_interface_sr.cpp 핸들러가 호출한다. 데몬은 세션마다 재생성되지 않고 일시정지(pause)와 재개(resume)를 반복한다.
3.1 부트 타임 진입점: cdc_daemons_init
섹션 제목: “3.1 부트 타임 진입점: cdc_daemons_init”cdc_daemons_init은 게이트 역할을 한다. CDC는 supplemental_log로 활성화하는 opt-in 기능이므로, 해당 값이 꺼져 있으면 즉시 반환한다 — 데몬도, 메모리도 없다.
// cdc_daemons_init -- src/transaction/log_manager.cvoid cdc_daemons_init (){ if (prm_get_integer_value (PRM_ID_SUPPLEMENTAL_LOG) == 0) return; /* <- CDC disabled: nothing allocated */ cdc_Logging = prm_get_bool_value (PRM_ID_CDC_LOGGING_DEBUG); /* <- enables cdc_log() tracing */ cdc_initialize (); /* <- zero state + allocate queue + wire temp_logbuf */ cdc_loginfo_producer_daemon_init (); /* <- create the cubthread daemon */}순서가 중요하다: cdc_initialize가 cdc_loginfo_producer_daemon_init보다 먼저 실행되어야 한다. 데몬 태스크 cdc_loginfo_producer_execute는 스케줄링되는 순간부터 producer.state / .request와 loginfo_queue 포인터를 읽기 때문이다.
불변 조건 — CDC 전역 상태는 데몬이 존재하기 전에 완전히 초기화된다.
cdc_initialize가 완료(state = CDC_PRODUCER_STATE_DEAD설정,loginfo_queue할당, 두temp_logbuf슬롯 정렬)된 이후에야create_daemon이 호출된다. 데몬의 첫 번째 동작은state/request와 큐 포인터를 읽는 것이다.loginfo_queue가 NULL이거나temp_logbuf가 정렬되지 않은 상태라면 첫 번째is_full()확인이나 페이지 memcpy에서 즉시 크래시가 발생한다.cdc_daemons_init의 호출 순서가 이 불변 조건을 강제한다.
cdc_daemons_init은 src/transaction/boot_sr.c의 boot_restart_server에서 호출되며, cdc_daemons_destroy는 같은 파일의 두 종료 경로에서 호출된다. 이 블록은 #if defined (SERVER_MODE)로 감싸져 있다 — standalone(SA) 모드에서는 프로듀서 데몬이 없다.
3.2 cdc_initialize — 상태 초기화와 큐 할당
섹션 제목: “3.2 cdc_initialize — 상태 초기화와 큐 할당”순수한 상태 설정이다. 블로킹도 없고 mutex도 건드리지 않는다(mutex는 이후 daemon-init에서 생성된다). 분기 없는 직선 코드이며, 각 대입은 Chapter 1의 필드와 1:1로 대응된다:
// cdc_initialize -- src/transaction/log_manager.c cdc_Gl.conn.fd = -1; cdc_Gl.conn.status = CONN_CLOSED; /* <- no consumer attached */ cdc_Gl.producer.request = CDC_REQUEST_PRODUCER_NONE; cdc_Gl.producer.state = CDC_PRODUCER_STATE_DEAD; cdc_Gl.loginfo_queue = new lockfree::circular_queue <CDC_LOGINFO_ENTRY *> (MAX_CDC_LOGINFO_QUEUE_ENTRY); cdc_Gl.producer.temp_logbuf[0].log_page_p = (LOG_PAGE *) PTR_ALIGN (cdc_Gl.producer.temp_logbuf[0].log_page, MAX_ALIGNMENT); /* <- slot 1 likewise */ // ... condensed: extraction_user/classoids = NULL; consumer.request = NONE; produced/consumed_queue_size = 0; // first/last_loginfo_queue_lsa, consumer.log_info*, start_lsa, next_lsa all LSA_SET_NULL / 0 ...세 가지 세부 사항이 있다: (1) 큐는 포인터를 보관한다 — MAX_CDC_LOGINFO_QUEUE_ENTRY = 2048개의 슬롯 상한선이며, 엔트리와 log_info 페이로드는 프로듀서가 별도로 힙 할당한다. 바이트 상한선 MAX_CDC_LOGINFO_QUEUE_SIZE = 32 MB는 produced_queue_size로 추적되며, 두 상한선 중 하나라도 초과하면 프로듀서가 일시정지된다. (2) 각 temp_logbuf[i].log_page_p는 log_page[IO_MAX_PAGE_SIZE + MAX_ALIGNMENT] 배열 내에서 MAX_ALIGNMENT로 정렬된 첫 번째 주소다 — pageid % 2로 인덱싱되는 2-페이지 윈도우. (3) tran_user / tran_ignore는 건드리지 않는다(정적 cdc_Gl에서 std::unordered_map 멤버로 기본 생성되어 비어 있음). cdc_finalize(§3.6)가 해당 내용을 해제한다.
3.3 cdc_loginfo_producer_daemon_init — 데몬 등록
섹션 제목: “3.3 cdc_loginfo_producer_daemon_init — 데몬 등록”CDC의 유일한 스레드가 여기서 탄생한다. 파일 스코프의 REGISTER_DAEMON (cdc_loginfo_producer) 매크로가 다중도(multiplicity) 1의 데몬 슬롯을 예약한다 — 프로듀서는 하나뿐이다.
// cdc_loginfo_producer_daemon_init -- src/transaction/log_manager.c assert (cdc_Loginfo_producer_daemon == NULL); /* <- invariant: exactly one daemon ever */ pthread_mutex_init (&cdc_Gl.producer.lock, NULL); pthread_cond_init (&cdc_Gl.producer.wait_cond, NULL); /* <- the producer's sleep primitive */ LSA_SET_NULL (&cdc_Gl.producer.next_extraction_lsa); cdc_Gl.producer.request = CDC_REQUEST_PRODUCER_TO_WAIT; /* <- born paused, set before create_daemon */ cubthread::looper looper = cubthread::looper (std::chrono::milliseconds (10)); /* <- 10 ms poll period */ cdc_Loginfo_producer_daemon = cubthread::get_manager ()->create_daemon (looper, new cubthread::entry_callable_task (cdc_loginfo_producer_execute), "cdc-loginfo-producer");mutex와 condvar는 여기서 생성된다 — cdc_initialize가 아니다. 따라서 시그널링 프리미티브(§3.8-§3.9)는 데몬이 존재한 이후에만 유효하다. request = TO_WAIT은 create_daemon 이전에 설정된다: 데몬 본체(Chapter 5)는 state = RUN을 설정하고, 첫 번째 반복에서 TO_WAIT를 확인한 뒤 state = WAIT로 설정하고 파킹된다 — 태어나자마자 일시정지 상태로, 컨슈머가 시작 LSA를 심어줄 때까지 추출을 수행하지 않는다. 10 ms 주기는 새 로그 레코드 감지와 LOG_HA_DUMMY_SERVER_STATUS 하트비트(Chapter 5)의 지연 상한선을 결정한다.
flowchart TD A["cdc_daemons_init"] -->|"supplemental_log == 0"| Z["반환, 아무것도 할당되지 않음"] A -->|"활성화됨"| B["cdc_Logging = prm CDC_LOGGING_DEBUG"] B --> C["cdc_initialize"] C --> C1["conn / 필터 / 카운터 초기화"] C --> C2["new circular_queue 2048 슬롯"] C --> C3["PTR_ALIGN temp_logbuf 0과 1"] C --> C4["state = DEAD, requests = NONE"] C --> D["cdc_loginfo_producer_daemon_init"] D --> D1["pthread_mutex_init + cond_init"] D --> D2["request = TO_WAIT, next_extraction_lsa = NULL"] D --> D3["create_daemon looper 10ms -> cdc_loginfo_producer_execute"] D3 --> E["데몬 파킹: wait_cond에서 state = WAIT"]
Figure 3-1: 부트 타임 초기화. 데몬은 시드 LSA를 기다리며 WAIT 상태로 파킹된다.
3.4 cdc_set_configuration — 추출 필터 설치
섹션 제목: “3.4 cdc_set_configuration — 추출 필터 설치”attach 시 network_interface_sr.cpp의 CDC 핸들러가 클라이언트의 필터 파라미터를 언패킹하고 이 함수를 호출한다. 세션 스코프이며, 큐 할당도 스레드 생성도 없다.
// cdc_set_configuration -- src/transaction/log_manager.c (straight-line, no branches) cdc_free_extraction_filter (); /* <- defensive: drop stale filter from a crashed prior session */ cdc_Gl.consumer.extraction_timeout = timeout; cdc_Gl.consumer.max_log_item = max_log_item; /* <- consumer */ cdc_Gl.producer.all_in_cond = all_in_cond; /* <- producer: filter combine mode */ cdc_Gl.producer.extraction_user = user; cdc_Gl.producer.num_extraction_user = num_user; /* <- takes ownership */ cdc_Gl.producer.extraction_classoids = classoids; cdc_Gl.producer.num_extraction_class = num_class;필터는 경계를 기준으로 나뉜다: 컨슈머는 extraction_timeout과 max_log_item(fetch 대기시간, RPC당 아이템 수)을 소유하고, 프로듀서는 all_in_cond, extraction_user[](cdc_is_filtered_user에서 사용, Chapter 6), extraction_classoids[](cdc_is_filtered_class)를 소유한다. 목록이 비어 있으면 “모두 통과”를 의미한다. user/classoids 포인터는 네트워크 언패킹에서 넘어오며, 이제부터 CDC가 소유권을 갖는다. 앞의 cdc_free_extraction_filter 호출은 필수적이다: 비정상 종료된 컨슈머는 teardown을 실행하지 못했으므로 이전 세션의 필터 배열이 남아 있을 수 있다. 먼저 재해제(re-free)함으로써 재접속을 멱등적으로 만든다.
3.5 Teardown: cdc_free_extraction_filter, cdc_cleanup, cdc_cleanup_consumer
섹션 제목: “3.5 Teardown: cdc_free_extraction_filter, cdc_cleanup, cdc_cleanup_consumer”중첩된 계층 구조를 가진다. cdc_free_extraction_filter가 가장 안쪽이며 필터 배열만 해제한다:
// cdc_free_extraction_filter -- src/transaction/log_manager.c if (cdc_Gl.producer.extraction_user != NULL) { for (int i = 0; i < cdc_Gl.producer.num_extraction_user; i++) /* <- bound is num_extraction_user */ if (cdc_Gl.producer.extraction_user[i] != NULL) free_and_init (cdc_Gl.producer.extraction_user[i]); /* <- each username string, then the array below */ free_and_init (cdc_Gl.producer.extraction_user); } if (cdc_Gl.producer.extraction_classoids != NULL) free_and_init (cdc_Gl.producer.extraction_classoids);free_and_init은 해제한 뒤 NULL로 설정하므로 이중 호출이 안전하다 — cdc_set_configuration의 재해제가 의존하는 특성이다. 사용자 수와 배열은 항상 함께 설정되므로 쌍이 유지된다.
cdc_cleanup은 세션 종료 경로다 — 프로듀서를 일시정지하고, 필터를 해제하고, 큐를 드레인하고, 카운터를 리셋한다:
// cdc_cleanup -- src/transaction/log_manager.c if (cdc_Gl.producer.state != CDC_PRODUCER_STATE_WAIT) cdc_pause_producer (); /* <- guard: skip if already parked, avoids needless round-trip */ cdc_free_extraction_filter (); while (!cdc_Gl.loginfo_queue->is_empty ()) /* <- drain: lock-free, no mutex needed */ { CDC_LOGINFO_ENTRY *tmp = NULL; cdc_Gl.loginfo_queue->consume (tmp); if (tmp->log_info != NULL) free (tmp->log_info); if (tmp != NULL) free (tmp); } /* <- payload then entry */ cdc_Gl.consumer.consumed_queue_size = 0; cdc_Gl.producer.produced_queue_size = 0; LSA_SET_NULL (&cdc_Gl.first_loginfo_queue_lsa); LSA_SET_NULL (&cdc_Gl.last_loginfo_queue_lsa); pthread_mutex_lock (&cdc_Gl.producer.lock); /* <- under lock: daemon reads next_extraction_lsa */ LSA_SET_NULL (&cdc_Gl.producer.next_extraction_lsa); /* in its hot loop (Ch.5), avoid torn read */ pthread_mutex_unlock (&cdc_Gl.producer.lock); cdc_cleanup_consumer (); /* <- inner: free the send buffer */cdc_cleanup_consumer는 리프 함수로, 서버→클라이언트 전송 버퍼만 해제한다. 독립적으로 호출해도 안전하다:
// cdc_cleanup_consumer -- src/transaction/log_manager.c if (cdc_Gl.consumer.log_info_size != 0) /* <- frees log_info only inside this branch */ { cdc_Gl.consumer.log_info_size = 0; cdc_Gl.consumer.num_log_info = 0; if (cdc_Gl.consumer.log_info != NULL) free_and_init (cdc_Gl.consumer.log_info); } LSA_SET_NULL (&cdc_Gl.consumer.start_lsa); LSA_SET_NULL (&cdc_Gl.consumer.next_lsa);log_info와 log_info_size는 항상 함께 변경되므로(Chapter 7) 가드가 올바르다. log_info를 할당하면서 log_info_size를 올리지 않으면 누수가 발생한다.
3.6 cdc_finalize와 cdc_daemons_destroy — 종료 경로
섹션 제목: “3.6 cdc_finalize와 cdc_daemons_destroy — 종료 경로”cdc_daemons_destroy는 boot 생명주기의 teardown으로, supplemental_log 확인과 NULL-데몬 가드로 이중 종료를 no-op으로 처리한다:
// cdc_daemons_destroy -- src/transaction/log_manager.c if (prm_get_integer_value (PRM_ID_SUPPLEMENTAL_LOG) == 0 || cdc_Loginfo_producer_daemon == NULL) return; /* <- disabled or already destroyed */ cdc_kill_producer (); /* <- request DEAD, spin until daemon confirms */ cubthread::get_manager ()->destroy_daemon (cdc_Loginfo_producer_daemon); cdc_finalize (); /* <- only now free queue + tran maps */불변 조건 — 데몬이 완전히 종료된 이후에만 메모리가 해제된다.
cdc_kill_producer는state == DEAD가 될 때까지 반환하지 않고,destroy_daemon은 스레드를 join한다. 이 두 단계가 완료된 이후에만cdc_finalize가loginfo_queue를 삭제하고tran_user문자열을 해제한다. 데몬이 아직 추출 중인 상태에서cdc_finalize가 실행되면 해제된 큐를 역참조하게 된다. kill → destroy → finalize 순서가 이 불변 조건을 강제한다. 순서를 바꾸면 use-after-free가 발생한다.
cdc_finalize는 cdc_cleanup과 대칭을 이루지만, 트랜잭션별 tran_user 문자열을 해제하고 큐를 드레인하는 대신 삭제한다:
// cdc_finalize -- src/transaction/log_manager.c cdc_free_extraction_filter (); for (auto iter:cdc_Gl.producer.tran_user) /* <- usernames captured per live tran */ if (iter.second != NULL) free_and_init (iter.second); if (cdc_Gl.loginfo_queue != NULL) { while (!cdc_Gl.loginfo_queue->is_empty ()) /* <- same drain as cleanup: payload + entry */ { /* ... consume tmp; free_and_init (tmp->log_info); free_and_init (tmp); ... */ } delete cdc_Gl.loginfo_queue; cdc_Gl.loginfo_queue = NULL; } /* <- destroy the ring itself */ cdc_Gl.consumer.consumed_queue_size = 0; cdc_Gl.producer.produced_queue_size = 0; pthread_mutex_lock (&cdc_Gl.producer.lock); /* <- next_extraction_lsa cleared under lock, as in cleanup */ LSA_SET_NULL (&cdc_Gl.producer.next_extraction_lsa); pthread_mutex_unlock (&cdc_Gl.producer.lock); LSA_SET_NULL (&cdc_Gl.last_loginfo_queue_lsa); LSA_SET_NULL (&cdc_Gl.first_loginfo_queue_lsa);cdc_cleanup(세션 종료)은 프로듀서를 일시정지하고, 큐를 드레인하되 유지하며, tran_user를 건드리지 않고, cdc_cleanup_consumer로 전송 버퍼를 해제한다. 반면 cdc_finalize(서버 종료)는 프로듀서가 kill된 이후 실행되어, 큐를 드레인한 뒤 delete하고, tran_user를 해제한다 — 이 함수가 실행된 후에는 신선한 cdc_initialize 없이는 전역 상태를 사용할 수 없다. 두 가지 비대칭이 있다: cdc_finalize는 cdc_cleanup_consumer를 호출하지 않으며(전송 버퍼는 detach 시 또는 OS 종료 시 해제됨), tran_ignore는 순회하지 않는다(int 값이므로 힙 해제 불필요).
3.7 cdc_reinitialize_queue — 큐를 유지한 채 re-seek
섹션 제목: “3.7 cdc_reinitialize_queue — 큐를 유지한 채 re-seek”세션 중 re-seek 요청이 오면 큐에 컨슈머가 더 이상 원하지 않는 엔트리가 있을 수 있다. teardown 후 재구성 대신, 제자리에서 트리밍 또는 리셋하고 is_queue_reinitialized를 설정해 프로듀서가 이를 인식하게 한다.
// cdc_reinitialize_queue -- src/transaction/log_manager.c CDC_LOGINFO_ENTRY *consume = NULL; if (cdc_Gl.producer.produced_queue_size == 0) goto end; /* <- branch 1: empty queue, nothing to trim */ cdc_Gl.is_queue_reinitialized = true; /* <- producer reads this to resync (Chapter 5) */ if (LSA_LT (&cdc_Gl.first_loginfo_queue_lsa, start_lsa) && LSA_GE (&cdc_Gl.last_loginfo_queue_lsa, start_lsa)) { /* branch 2: start_lsa inside [first,last] -> trim the front only */ LOG_LSA next_consume_lsa; LSA_COPY (&next_consume_lsa, &cdc_Gl.first_loginfo_queue_lsa); while (LSA_LT (&next_consume_lsa, start_lsa)) { cdc_Gl.loginfo_queue->consume (consume); cdc_Gl.consumer.consumed_queue_size += consume->length; LSA_COPY (&next_consume_lsa, &consume->next_lsa); if (consume->log_info != NULL) free_and_init (consume->log_info); } /* <- payload only, not the entry */ cdc_Gl.producer.produced_queue_size -= cdc_Gl.consumer.consumed_queue_size; /* <- keep gauge accurate */ cdc_Gl.consumer.consumed_queue_size = 0; } else { /* branch 3: start_lsa outside range -> drain everything, delete + new the ring */ while (!cdc_Gl.loginfo_queue->is_empty ()) { cdc_Gl.loginfo_queue->consume (consume); if (consume->log_info != NULL) free_and_init (consume->log_info); } cdc_Gl.producer.produced_queue_size = 0; cdc_Gl.consumer.consumed_queue_size = 0; delete cdc_Gl.loginfo_queue; cdc_Gl.loginfo_queue = new lockfree::circular_queue <CDC_LOGINFO_ENTRY *> (MAX_CDC_LOGINFO_QUEUE_ENTRY); }end: cdc_log ("... reinitialize end");Branch 2는 비용이 낮은 ‘약간 앞으로 re-seek’ 케이스이고, branch 3는 first 이전으로의 역방향 seek 또는 last를 훨씬 지나치는 대형 forward seek를 처리한다. 두 경우 모두 트리밍 루프에서 consume->log_info는 해제하지만 consume 엔트리 구조체 자체는 해제하지 않는다 — cdc_cleanup/cdc_finalize와 달리 둘 다 해제한다. 소스의 // TODO: please check consume is NULL 주석이 이 부분을 rough edge로 표시하고 있으니, 코드를 하드닝할 때 주의해야 한다. is_queue_reinitialized는 여기서 클리어되지 않는다 — 프로듀서가 추출 LSA를 재동기화한 후 직접 클리어한다(Chapter 5).
3.8 프로듀서 일시정지 및 종료 (컨슈머 → 프로듀서)
섹션 제목: “3.8 프로듀서 일시정지 및 종료 (컨슈머 → 프로듀서)”세 함수가 producer.request를 조작하고 producer.state를 읽는다. 핵심 특성은 긴밀한 핑퐁이 없다는 것이다. 컨슈머는 request를 설정하고 fire-and-forget(wakeup)이거나 state를 바쁜 대기(busy-wait)로 확인한다(pause/kill). 데몬은 state를 소유하고, 컨슈머는 request를 소유한다.
// cdc_pause_producer / cdc_wakeup_producer / cdc_kill_producer -- src/transaction/log_manager.ccdc_pause_producer: cdc_Gl.producer.request = CDC_REQUEST_PRODUCER_TO_WAIT; /* <- set request */ while (cdc_Gl.producer.state != CDC_PRODUCER_STATE_WAIT) sleep (1); /* <- spin on state, no signal */cdc_wakeup_producer: cdc_Gl.producer.request = CDC_REQUEST_PRODUCER_NONE; pthread_cond_signal (&cdc_Gl.producer.wait_cond); /* <- fire-and-forget nudge */cdc_kill_producer: cdc_Gl.producer.request = CDC_REQUEST_PRODUCER_TO_BE_DEAD; while (cdc_Gl.producer.state != CDC_PRODUCER_STATE_DEAD) { pthread_cond_signal (&cdc_Gl.producer.wait_cond); sleep (1); } /* <- signal in loop: may be parked */이 비대칭은 의도적이다. cdc_pause_producer는 시그널을 보내지 않는다 — 데몬은 아직 루프 중이므로(10 ms) 다음 틱에서 TO_WAIT를 자연스럽게 확인한다. cdc_kill_producer는 스핀 내부에서 반드시 시그널을 보내야 한다 — 데몬이 이미 wait_cond에 파킹되어 있을 수 있기 때문이다(예: 백프레셔). 그 상태에서 request만 바꿔도 데몬이 이를 인식할 수 없으므로, 매초 시그널을 보내 데몬을 깨워 request를 재확인하게 한다. cdc_wakeup_producer는 pause의 역방향 동작으로, re-seed 이후 사용된다(Chapter 4).
불변 조건 — request와 state는 절대 서로 쓰지 않는다. 컨슈머는
producer.request만 쓰고, 데몬은producer.state만 쓴다. 두 필드 모두volatile이다. 이 단일 쓰기 규율이 pause/kill의 바쁜 대기가 mutex 없이도 올바른 이유다: 각 측은 정확히 한 개의 다른 스레드가 쓰는 필드만 읽는다. 이를 위반하면 — 예를 들어 컨슈머가state를 직접 설정하면 — 바쁜 대기가 교착 상태에 빠지거나 전환을 놓칠 수 있다.
3.9 컨슈머 일시정지 (프로듀서 → 컨슈머)
섹션 제목: “3.9 컨슈머 일시정지 (프로듀서 → 컨슈머)”역방향은 훨씬 단순하다. 프로듀서가 consumer.request를 뒤집기만 한다. condvar도 없고 바쁜 대기도 없다 — 컨슈머는 자신의 fetch 주기에 맞춰 플래그를 확인한다:
// cdc_pause_consumer / cdc_wakeup_consumer -- src/transaction/log_manager.cvoid cdc_pause_consumer () { cdc_Gl.consumer.request = CDC_REQUEST_CONSUMER_TO_WAIT; }void cdc_wakeup_consumer () { cdc_Gl.consumer.request = CDC_REQUEST_CONSUMER_TO_RUN; }프로듀서의 백프레셔 경로에서 사용된다(Chapter 5): produced_queue_size가 32 MB를 초과하거나 링이 가득 차면 프로듀서는 파킹 전에 cdc_pause_consumer를 호출하고, 큐가 소진된 후 cdc_wakeup_consumer를 호출한다. 어디까지나 advisory다 — 컨슈머는 플래그를 확인하기 전에 진행 중인 fetch를 마칠 수 있다.
stateDiagram-v2 direction LR [*] --> DEAD : cdc_initialize가 state를 DEAD로 설정 DEAD --> RUN : 데몬 시작, state를 RUN으로 설정 RUN --> WAIT : TO_WAIT 요청 확인 후 wait_cond에 파킹 WAIT --> RUN : wakeup_producer가 cond에 시그널 RUN --> DEAD : TO_BE_DEAD 요청 확인 후 루프 종료 WAIT --> DEAD : kill이 cond에 시그널 후 TO_BE_DEAD 확인
Figure 3-2: 프로듀서 상태 기계. state는 데몬이 소유하며, 전환은 컨슈머가 소유하는 request를 데몬이 폴링함으로써 구동된다.
attach 전체 시퀀스는 §3.1-§3.9를 하나로 연결한다: 서버 부트 시 cdc_daemons_init이 한 번 실행되어 큐와 데몬을 생성하고 WAIT 상태로 파킹한다. 각 attach마다 핸들러가 cdc_set_configuration을 호출하고 시작 LSA를 심어(Chapter 4) cdc_wakeup_producer를 호출한다. detach 시 cdc_cleanup이 실행되어 큐가 유지된다. 서버 종료 시 cdc_daemons_destroy → kill → destroy → cdc_finalize 순으로 큐가 삭제된다. 세션마다 스레드는 생성되지 않는다.
3.10 챕터 요약 — 핵심 정리
섹션 제목: “3.10 챕터 요약 — 핵심 정리”- 부트업은 두 단계로 순서가 정해져 있다.
cdc_daemons_init은cdc_initialize(큐 할당,temp_logbuf정렬, 상태 초기화)를cdc_loginfo_producer_daemon_init(mutex/condvar + 단일 데몬 생성)보다 엄격히 먼저 실행하므로, 데몬은 초기화되지 않은 상태를 절대 관측하지 않는다. - 데몬은 하나이며 태어나자마자 일시정지된다.
daemon_init은create_daemon이전에request = TO_WAIT를 설정하므로, 데몬 본체는state = RUN으로 진입한 뒤 즉시TO_WAIT를 확인하고wait_cond에 파킹된다. 컨슈머가 LSA를 심고cdc_wakeup_producer를 호출할 때까지 추출을 수행하지 않는다. - 필터는 세션 스코프이며 소유권이 이전된다.
cdc_set_configuration은 네트워크 언패킹된 배열을 저장하고, 방어적으로 기존 필터를 먼저 재해제하므로 비정상 종료 클라이언트 이후의 재접속이 멱등적으로 동작한다. - Teardown은 생명주기 기준으로 중첩된다.
cdc_free_extraction_filter⊂cdc_cleanup_consumer⊂cdc_cleanup(세션: 일시정지, 드레인, 큐 유지) ⊂cdc_finalize(서버 종료: kill, tran_user 해제, 큐delete). 큐의 생존 여부에 따라 선택한다. cdc_reinitialize_queue는 세 분기로 제자리 re-seek를 수행한다. 빈 큐(no-op), 범위 내(앞단 트리밍 + 바이트 계량 조정), 범위 외(delete+new).is_queue_reinitialized를 설정하며, 트리밍 루프는 페이로드만 해제하고 엔트리 구조체는 해제하지 않는다(TODO로 표시됨).- request와 state는 필드별 단일 쓰기 규율을 따른다.
producer.request는 컨슈머만,producer.state는 데몬만 쓰며 둘 다volatile이다. 이 때문에 pause/kill은 mutex 없이state를 바쁜 대기로 확인할 수 있다. - pause와 kill은 시그널링 방식이 다르다.
cdc_pause_producer는 request만 설정하고 폴링한다(state가 이미 WAIT이면 no-op).cdc_kill_producer는 스핀 내부에서wait_cond에 시그널을 보낸다 — 대상이 이미 파킹되어 있을 수 있기 때문이다.
Chapter 4: 시작 LSA 설정과 유효성 검증
섹션 제목: “Chapter 4: 시작 LSA 설정과 유효성 검증”프로듀서 데몬(3장)은 어디서부터 로그를 읽어야 하는지를 알기 전까지 깨어 있어도 아무 일도 하지 않는다. 이 챕터는 다음 질문에 답한다: 컨슈머는 시작 LSA를 어떻게 결정하고, 서버는 그 LSA를 읽어도 안전하다는 것을 어떻게 증명하는가? 컨슈머가 시작 지점을 지정하는 방법은 두 가지이며, 두 경로 모두 cdc_set_extraction_lsa로 수렴한다:
- 명시적 LSA — 다운스트림에 내구적으로 커밋된 LSA부터 재개;
cdc_validate_lsa로 게이팅된다. - 벽시계 시각 — “어제 14:00 근처부터 시작”;
cdc_find_lsa가 변환하며, 활성 볼륨과 아카이브 볼륨을 스캔해at_time이 요청 시각을 포괄하는 레코드를 찾는다.
유효성 검증 체인은 아카이브 삭제기(archive remover)가 준수하는 동일한 워터마크(cubrid-cdc.md §“Archive retention”)로 이어진다.
4.1 두 가지 시드 소스와 디스패치 경로
섹션 제목: “4.1 두 가지 시드 소스와 디스패치 경로”network_interface_sr.cpp가 두 요청을 각각의 핸들러로 라우팅한다. scdc_find_lsa(시각 경로)는 cdc_find_lsa를 호출하고, 조정된 결과를 수용한 뒤, §4.8의 pause/reinit/wakeup 시퀀스를 실행하고 응답을 패킹한다. scdc_get_loginfo_metadata(명시적 경로)는 먼저 유효성을 검증하고 성공한 경우에만 시드를 설정한다.
// scdc_find_lsa -- src/communication/network_interface_sr.cpperror_code = cdc_find_lsa (thread_p, &input_time, &start_lsa);if (error_code == NO_ERROR || error_code == ER_CDC_ADJUSTED_LSA) /* <- adjusted is OK */ { if (cdc_Gl.producer.state != CDC_PRODUCER_STATE_WAIT) cdc_pause_producer (); cdc_set_extraction_lsa (&start_lsa); // ... cdc_reinitialize_queue / cdc_wakeup_producer, see 4.8 ... ptr = or_pack_int (reply, error_code); /* <- pack AFTER the daemon calls */ ptr = or_pack_log_lsa (ptr, &start_lsa); or_pack_int64 (ptr, input_time); }else goto error;
// scdc_get_loginfo_metadata -- src/communication/network_interface_sr.cppif (LSA_ISNULL (&cdc_Gl.consumer.next_lsa)) /* <- no cursor yet (fresh / restarted) */ { error_code = cdc_validate_lsa (thread_p, &start_lsa); /* must pass exactly */ if (error_code != NO_ERROR) goto error; /* <- reject: archive gone, bad LSA */ cdc_set_extraction_lsa (&start_lsa); }ER_CDC_ADJUSTED_LSA는 여기서 오류가 아니다: 알림(ER_NOTIFICATION_SEVERITY)이다 — “정확한 요청을 충족할 수 없으므로 가장 가까운 LSA를 반환한다” — 조정된 LSA와 시각과 함께 응답에 패킹되어 클라이언트에는 CUBRID_LOG_SUCCESS_WITH_ADJUSTED_LSA(§4.6)로 노출된다.
flowchart LR T["시각으로 LSA 설정"] --> FL["cdc_find_lsa"] X["명시적 LSA 설정"] --> VL["cdc_validate_lsa"] FL -->|NO_ERROR or ADJUSTED_LSA| SE["cdc_set_extraction_lsa"] FL -->|하드 오류| ERR["오류 응답"] VL -->|NO_ERROR| SE VL -->|INVALID_LOG_LSA / ER_FAILED| ERR SE --> NEXT["next_extraction_lsa + consumer.next_lsa"]
Figure 4-1 — 두 시드 소스가 cdc_set_extraction_lsa로 수렴한다. 시각 기반 시딩은 조정된 LSA를 허용하지만, 명시적 시딩은 cdc_validate_lsa를 정확히 통과해야 한다.
4.2 cdc_set_extraction_lsa — 커서 시딩
섹션 제목: “4.2 cdc_set_extraction_lsa — 커서 시딩”// cdc_set_extraction_lsa -- src/transaction/log_manager.cpthread_mutex_lock (&cdc_Gl.producer.lock);LSA_COPY (&cdc_Gl.producer.next_extraction_lsa, lsa); /* <- producer cursor */pthread_mutex_unlock (&cdc_Gl.producer.lock);LSA_COPY (&cdc_Gl.consumer.next_lsa, lsa); /* <- consumer cursor */이 함수는 커서를 두 개 기록한다. next_extraction_lsa(producer.lock 하에서, 프로듀서가 매 틱마다 읽기 때문 — 5장)는 전진 탐색의 시작점이다. consumer.next_lsa는 컨슈머가 다음 배치의 시작으로 기대하는 지점으로, scdc_get_loginfo_metadata 앞부분의 LSA_EQ (&cdc_Gl.consumer.next_lsa, &start_lsa) 가드가 이를 사용해 동기화 이탈(out-of-sync) 재개를 감지한다.
핵심 불변 조건: consumer.start_lsa는 여기서 시딩되지 않는다. 세 번째 커서(1장)는 아카이브 삭제기 워터마크(§4.7)이며, cdc_make_loginfo에서 첫 번째 배치를 내보낼 때까지 cdc_initialize 값(LSA_SET_NULL)을 유지한다. 이 분리 덕분에 의도(LSA X부터 시작)와 가장 오래된 in-flight log info가 X에 있다는 확정이 구별된다: 만약 이 함수가 start_lsa도 같이 이동시킨다면, 역방향 재시딩(backward reseed)이 새 위치에 여전히 존재하는 log info를 건너뛰어 워터마크를 뒤로 당기게 된다.
4.3 cdc_find_lsa — 타임스탬프를 LSA로 변환
섹션 제목: “4.3 cdc_find_lsa — 타임스탬프를 LSA로 변환”cdc_find_lsa (thread_p, *extraction_time, *start_lsa)는 벽시계 time_t(입출력)를 받아 요청 이후 가장 가까운 LSA를 반환하며, 조정 시 *extraction_time을 덮어쓴다. 활성 볼륨을 먼저 탐색하고, 이후 아카이브를 최신순(newest-to-oldest)으로 걷는다:
// cdc_find_lsa -- src/transaction/log_manager.cint begin = log_Gl.hdr.last_deleted_arv_num; /* <- one before oldest kept archive */int end = log_Gl.hdr.nxarv_num - 1; /* <- newest archive number */error = cdc_get_start_point_from_file (thread_p, -1, &ret_lsa, &active_start_time); /* probe active */if (error == ER_FAILED || error == ER_LOG_READ) goto end; /* (1) hard read failure */else if (active_start_time != 0 && active_start_time <= *extraction_time) { /* (2) lands in active -> fine-scan; (2b) LSA_NOT_FOUND -> prev_lsa, time=now, ADJUSTED */ }else { /* (3/4/5) older than active start -> scan archives, else adjust to oldest */ }모든 분기는 세 가지 결과 중 하나로 귀결된다 — 정확한 매칭(NO_ERROR), 조정(ER_CDC_ADJUSTED_LSA + 알림), 하드 오류(ER_FAILED / ER_LOG_READ):
| # | 조건 | 동작 | 반환값 |
|---|---|---|---|
| 1 | 활성 볼륨 탐색에서 ER_FAILED/ER_LOG_READ | goto end | 해당 오류 |
| 2a | 요청 ≥ 활성 볼륨 시작, 정확한 매칭 발견 | ret_lsa 시드 | NO_ERROR |
| 2b | 요청 ≥ 활성 볼륨 시작, 최신 로그보다 미래 | log_Gl.append.prev_lsa 시드, time = now | ER_CDC_ADJUSTED_LSA |
| 3 | 요청이 활성 볼륨 시작보다 오래됨, num_arvs > 0 | 아카이브를 최신→오래된 순으로 스캔(아래) | varies |
| 4 | 활성 볼륨 시작보다 오래됨, num_arvs == 0, 활성에 시각 있음 | 가장 오래된 활성 LSA 시드, 시각 조정 | ER_CDC_ADJUSTED_LSA |
| 5 | num_arvs == 0, 활성 볼륨에 시각 보유 레코드 없음 | log_Gl.append.prev_lsa 시드, time = now | ER_CDC_ADJUSTED_LSA |
아카이브 스캔(분기 3)은 end에서 begin 바로 위까지 내려가며 볼륨 하나를 포위(cdc_get_start_point_from_file, §4.4)한 뒤, 그 안을 세밀 스캔(cdc_get_lsa_with_start_point, §4.5)한다:
// cdc_find_lsa -- src/transaction/log_manager.c (archive scan)for (int i = end; i > begin; i--) { error = cdc_get_start_point_from_file (thread_p, i, &ret_lsa, &archive_start_time); if (error != NO_ERROR) goto end; /* (3-err) propagate read error */ if (archive_start_time <= *extraction_time) { target_arv_num = i; break; } /* bracket */ }if (target_arv_num == -1) /* (3a) older than every archive */ { LSA_COPY (start_lsa, &ret_lsa); error = ER_CDC_ADJUSTED_LSA; } /* <- oldest LSA we have */else /* (3b) bracketed -> fine-scan */ { error = cdc_get_lsa_with_start_point (thread_p, extraction_time, &ret_lsa); if (error != NO_ERROR) error = ER_CDC_ADJUSTED_LSA; LSA_COPY (start_lsa, &ret_lsa); }먼저 포위(볼륨당 헤더 읽기 1회)하므로 모든 아카이브를 전진 스캔하는 비용을 피할 수 있다 — §4.4와 §4.5로 두 단계로 나눈 이유가 바로 이것이다.
4.4 cdc_get_start_point_from_file — 볼륨의 첫 번째 시각
섹션 제목: “4.4 cdc_get_start_point_from_file — 볼륨의 첫 번째 시각”이 헬퍼는 “볼륨 arv_num의 첫 번째 타임스탬프 있는 LSA는 무엇인가?”에 답한다(arv_num == -1은 활성 볼륨). 볼륨의 첫 번째 페이지에 접근하는 방법은 세 가지 — 활성 헤더, 이미 마운트된 아카이브 헤더, 또는 아카이브를 마운트하여 LOG_ARV_HEADER를 읽기:
// cdc_get_start_point_from_file -- src/transaction/log_manager.c (page selection)if (arv_num == -1) { process_lsa.pageid = log_Gl.hdr.fpageid; process_lsa.offset = 0; } /* active */else if (log_Gl.archive.vdes != NULL_VOLDES && log_Gl.archive.hdr.arv_num == arv_num) { process_lsa.pageid = log_Gl.archive.hdr.fpageid; ... } /* already mounted */else if (fileio_is_volume_exist (arv_name) == true) { vdes = fileio_mount (...); if (vdes != NULL_VOLDES) /* <- mount succeeded */ { if (fileio_read (...) == NULL) { ...; return ER_LOG_READ; } if (difftime64 (arv_hdr->db_creation, log_Gl.hdr.db_creation) != 0) { ...; return ER_LOG_DOESNT_CORRESPOND_TO_DATABASE; } /* <- wrong DB's archive */ process_lsa.pageid = arv_hdr->fpageid; ... } }db_creation 검사는 다른 데이터베이스에 속한 아카이브를 거부한다. 아카이브가 디스크에 없거나 fileio_mount가 NULL_VOLDES를 반환하면, process_lsa는 LSA_INITIALIZER로 남아 다음 가드가 발동된다. 이후 스캔은 forw_lsa를 따라 첫 번째 시각 보유 레코드를 찾는다:
// cdc_get_start_point_from_file -- src/transaction/log_manager.c (scan for first time)if (LSA_ISNULL (&process_lsa)) /* file missing / mount failed */ { assert (!LSA_ISNULL (&process_lsa)); /* <- debug build aborts here */ er_set (..., ER_CDC_LSA_NOT_FOUND, ...); return ER_CDC_LSA_NOT_FOUND; }if ((error_code = logpb_fetch_page (...)) != NO_ERROR) return error_code;while (!LSA_ISNULL (&process_lsa)) /* forward-walk forw_lsa */ { log_type = log_rec_header->type; /* COMMIT/ABORT/HA_DUMMY carry at_time */ LOG_READ_ADD_ALIGN (thread_p, sizeof (*log_rec_header), &process_lsa, log_pgptr); /* on a matched type: LSA_COPY (ret_lsa, &cur_log_lsa); *time = at_time; return NO_ERROR; */ /* page boundary + LSA_ISNULL (&forw_lsa) -> ER_CDC_LSA_NOT_FOUND, else refetch page */ LSA_COPY (&process_lsa, &forw_lsa); }null-LSA 블록 첫 줄의 assert (!LSA_ISNULL (&process_lsa))에 주목하라: 마운트 실패/파일 없음 경로는 발생해서는 안 되는 상황으로, 디버그 빌드에서 abort된다. ER_CDC_LSA_NOT_FOUND 반환은 릴리즈 빌드의 폴백일 뿐, 설계된 정상 경로가 아니다. at_time을 사용할 수 있는 레코드 타입은 세 가지 — LOG_COMMIT, LOG_ABORT(LOG_REC_DONETIME), LOG_DUMMY_HA_SERVER_STATE(LOG_REC_HA_SERVER_STATE) — 뿐이다. 루프는 하나를 찾으면(NO_ERROR를 반환) 종료하거나, 페이지 경계에서 LSA_ISNULL (&forw_lsa)가 되면(ER_CDC_LSA_NOT_FOUND) 종료한다. LOG_READ_ADD_ALIGN은 log_reader가 래핑하는 것과 동일한 정렬 헬퍼다(cubrid-cdc.md §“Log walker”).
4.5 cdc_get_lsa_with_start_point — 볼륨 내 세밀 스캔
섹션 제목: “4.5 cdc_get_lsa_with_start_point — 볼륨 내 세밀 스캔”§4.4가 포위를 위해 볼륨의 첫 번째 시각을 찾는다면, 이 헬퍼는 알려진 시작 LSA부터 전진 스캔하여 시각 ≥ 요청인 첫 번째 레코드를 찾는다(*time과 *start_lsa 모두 입출력). §4.4의 레코드별 처리 로직을 재사용하되, >= *time 매칭 비교만 다르다:
// cdc_get_lsa_with_start_point -- src/transaction/log_manager.cif (logpb_fetch_page (...) != NO_ERROR) return ER_FAILED; /* page fetch failed */while (!LSA_ISNULL (&process_lsa)) { if (at_time >= *time) /* first record at/after request */ { *time = at_time; LSA_COPY (start_lsa, ¤t_lsa); return NO_ERROR; } LSA_COPY (&process_lsa, &forw_lsa); } /* else advance / refetch page */return ER_CDC_LSA_NOT_FOUND; /* ran off the end of the log */세 가지 분기 — ER_FAILED(fetch 실패), NO_ERROR(매칭), ER_CDC_LSA_NOT_FOUND(로그 끝까지 탐색, 페이지 경계에서 forw_lsa가 null인 경우도 포함). >=를 사용하기 때문에 CDC 시작 시각은 “요청 시각 이후(at or after)“다. 분기 2b/3b에서 여기서 LSA_NOT_FOUND가 발생하면 ER_CDC_ADJUSTED_LSA로 강등되고 호출자가 최신 LSA를 대신 사용한다.
4.6 유효성 검증 체인 — cdc_validate_lsa와 cdc_check_lsa_range
섹션 제목: “4.6 유효성 검증 체인 — cdc_validate_lsa와 cdc_check_lsa_range”명시적 LSA 경로에서는 시드가 실제로 읽을 수 있음이 증명되어야 한다. cdc_validate_lsa는 네 개의 게이트를 실행한 후 레코드 경계 워크를 수행한다:
// cdc_validate_lsa -- src/transaction/log_manager.cif (LSA_ISNULL (lsa)) { ...; return ER_CDC_INVALID_LOG_LSA; } /* (1) null LSA */if (lsa->pageid >= LOGPAGEID_MAX) { ...; return ER_CDC_INVALID_LOG_LSA; } /* (2) absurd pageid */if (cdc_check_lsa_range (thread_p, lsa) != NO_ERROR) /* (3) outside kept range */ return ER_CDC_INVALID_LOG_LSA;if (logpb_fetch_page (thread_p, lsa, LOG_CS_SAFE_READER, log_page_p) != NO_ERROR) /* (4) */ return ER_FAILED;/* (5) walk record headers on the page; LSA_EQ a record start -> NO_ERROR, else INVALID_LOG_LSA */게이트 (3), cdc_check_lsa_range는 읽기 가능한 윈도우의 경계를 계산하고 포함 여부를 확인한다. §4.4와 동일한 mount/read/db_creation 패턴으로 가장 오래된 유지 아카이브의 헤더를 읽는다(동일한 ER_LOG_READ / ER_LOG_DOESNT_CORRESPOND_TO_DATABASE 오류 처리):
// cdc_check_lsa_range -- src/transaction/log_manager.cint begin = log_Gl.hdr.last_deleted_arv_num + 1; /* <- oldest STILL-kept archive */LOG_LSA nxio_lsa = log_Gl.append.get_nxio_lsa (); /* <- appended tail (exclusive upper) */if (num_arvs == 0) { first_lsa.pageid = log_Gl.hdr.fpageid; } /* nothing archived yet */else if (fileio_is_volume_exist (arv_name) == true) { /* mount + read header (else ER_LOG_READ / ER_LOG_DOESNT_CORRESPOND_TO_DATABASE) */ first_lsa.pageid = arv_hdr->fpageid; } /* oldest kept archive's first page */else { first_lsa.pageid = log_Gl.hdr.fpageid; } /* archive file gone */if (LSA_GE (lsa, &first_lsa) && LSA_LT (lsa, &nxio_lsa)) /* <- [first_lsa, nxio_lsa) */ return NO_ERROR;return ER_CDC_INVALID_LOG_LSA;핵심 불변 조건: 읽기 가능한 윈도우는 반개방 구간 [first_lsa, nxio_lsa)이다 (하한 = 위 블록에 따라 가장 오래된 유지 아카이브의 첫 번째 페이지; 상한 = 어펜더가 다음에 쓸 exclusive 테일). first_lsa 아래의 시드는 해당 아카이브가 이미 삭제되었음을 의미하고(§4.7), nxio_lsa 이상의 시드는 아직 쓰이지 않은 로그를 가리킨다. 둘 다 ER_CDC_INVALID_LOG_LSA로 거부된다 — 삭제된 아카이브가 오래된 재개 LSA를 무효화하는 구체적인 이유다.
게이트 (5)는 LSA가 실제 레코드 경계를 가리키는지 확인한다 — 루프가 가져온 페이지의 헤더를 걸으면서 LSA_EQ (&process_lsa, lsa)이면(NO_ERROR), 페이지를 벗어나면(ER_CDC_INVALID_LOG_LSA) 종료한다. Figure 4-2는 다섯 가지 분기를 모두 정리한다: 네 가지는 ER_CDC_INVALID_LOG_LSA로, 페이지 fetch 실패는 ER_FAILED로 거부된다.
flowchart TB
A["cdc_validate_lsa(lsa)"] --> B{"LSA가 null인가?"}
B -->|예| E1["ER_CDC_INVALID_LOG_LSA"]
B -->|아니오| C{"pageid >= LOGPAGEID_MAX?"}
C -->|예| E1
C -->|아니오| D["cdc_check_lsa_range"]
D -->|"[first_lsa, nxio_lsa) 범위 밖"| E1
D -->|범위 안| F{"logpb_fetch_page 성공?"}
F -->|아니오| E2["ER_FAILED"]
F -->|예| G["페이지 레코드 헤더 워크"]
G -->|"LSA_EQ a record start"| OK["NO_ERROR"]
G -->|"페이지 이탈, 매칭 없음"| E1
Figure 4-2 — cdc_validate_lsa 게이트 순서. cdc_check_lsa_range는 시드 LSA를 보유하는 아카이브가 삭제되었을 때 실패하는 게이트다.
클라이언트 측에서 cubrid_log_find_start_lsa가 응답을 디코딩한다. 첫 번째 분기가 NO_ERROR와 ER_CDC_ADJUSTED_LSA 모두를 잡아 CUBRID_LOG_SUCCESS를 반환하기 때문에, CUBRID_LOG_SUCCESS_WITH_ADJUSTED_LSA 분기는 현재 코드에서 도달 불가능하다(Cross-check 참고, 9장).
4.7 cdc_min_log_pageid_to_keep — 워터마크가 커서인 이유
섹션 제목: “4.7 cdc_min_log_pageid_to_keep — 워터마크가 커서인 이유”아카이브 삭제기(cubrid-cdc.md §“Archive retention”)는 이 함수를 호출해 CDC 컨슈머가 여전히 필요로 하는 가장 작은 pageid를 알아낸다:
// cdc_min_log_pageid_to_keep -- src/transaction/log_manager.creturn cdc_Gl.consumer.start_lsa.pageid;§4.2의 불변 조건에 따라 consumer.start_lsa는 cdc_make_loginfo에 의해서만 전진한다. 첫 번째 배치 전에는 LSA_SET_NULL 상태이며, 삭제기는 pageid NULL_PAGEID(-1)를 “제약 없음”으로 읽는다. 배치가 존재하면 start_lsa.pageid는 아직 전송되지 않은 log info가 위치한 가장 작은 페이지이므로, 삭제기는 그 페이지 이후의 모든 아카이브를 보존한다. 역방향 시드(backward seed)는 start_lsa를 뒤로 이동시킬 수 있기 때문에, 반드시 먼저 cdc_validate_lsa(§4.6)를 통과해야 한다.
Cross-check / 버전 드리프트: 동반 문서(cubrid-cdc.md §“Archive retention”)는 컨슈머가 없을 때 MAX_LOG_PAGEID를 반환한다고 서술하지만, 현재 소스는 그렇지 않다 — consumer.start_lsa.pageid를 무조건 반환하며 null-pageid 센티넬에 의존한다. 동반 문서의 표현은 의도된 계약이지, 실제 코드가 아니다.
4.8 next_extraction_lsa가 프로듀서 루프에 전달되는 방법
섹션 제목: “4.8 next_extraction_lsa가 프로듀서 루프에 전달되는 방법”cdc_set_extraction_lsa가 producer.next_extraction_lsa를 기록한 후, 핸들러들은 cdc_reinitialize_queue(새 시드보다 앞서 이미 생산된 log info 폐기)와 cdc_wakeup_producer(프로듀서를 WAIT에서 깨움)를 호출한다. 시각 경로는 §4.1 발췌문에 나타난 것처럼 먼저 cdc_pause_producer를 추가로 호출하여 시드가 실행 중인 틱과 경쟁하지 않도록 한다. 다음 틱에서 프로듀서는 next_extraction_lsa를 탐색 시작점으로 읽는다(5장) — 컨슈머의 “여기서 시작” 의도와 프로듀서의 전진 탐색 사이의 단일 핸드오프.
4.9 챕터 요약 — 핵심 정리
섹션 제목: “4.9 챕터 요약 — 핵심 정리”- 두 시드 소스 모두
cdc_set_extraction_lsa로 수렴하며,producer.next_extraction_lsa(producer.lock하에서)와consumer.next_lsa를 기록하지만, 의도적으로consumer.start_lsa는 건드리지 않는다. cdc_find_lsa는 2단계 시각→LSA 조회다 — 볼륨 포위(cdc_get_start_point_from_file), 그 안을 세밀 스캔(cdc_get_lsa_with_start_point) — 모든 분기는 정확한 매칭, 조정(ER_CDC_ADJUSTED_LSA), 또는 하드 오류를 반환한다.at_time을 담을 수 있는 레코드 타입은LOG_COMMIT,LOG_ABORT,LOG_DUMMY_HA_SERVER_STATE뿐이며, 스캔은>=(이후 포함)를 사용한다.cdc_get_start_point_from_file에서 파일 없음 분기는 assert로 가드된다.cdc_validate_lsa는 네 개의 게이트 — null, 비정상 pageid,cdc_check_lsa_range, 페이지 fetch — 와 레코드 경계 워크를 실행하며, 어느 것이든 실패하면 시드를 거부한다(대부분ER_CDC_INVALID_LOG_LSA).- 읽기 가능한 윈도우는 반개방 구간
[first_lsa, nxio_lsa)이며,first_lsa아래의 시드는 “아카이브가 삭제된” 경우다. cdc_min_log_pageid_to_keep은consumer.start_lsa.pageid를 반환한다(cdc_make_loginfo에 의해서만 전진). 따라서 역방향 재시딩은 반드시 먼저cdc_validate_lsa를 통과해야 한다.- 버전 드리프트: 동반 문서의 “컨슈머 없을 때
MAX_LOG_PAGEID”는 의도된 계약이며, 코드는 null-pageid 센티넬에 의존한다. 클라이언트의SUCCESS_WITH_ADJUSTED_LSA분기는 현재 도달 불가능하다.
Chapter 5: 프로듀서 데몬 외부 루프
섹션 제목: “Chapter 5: 프로듀서 데몬 외부 루프”이 챕터는 cdc_loginfo_producer_execute(log_manager.c)를 해부한다.
이 함수가 바로 cdc-loginfo-producer 데몬이 실행하는 본체다. 상위 수준의
동반 문서(cubrid-cdc.md, “Producer/consumer split”과 “Backpressure” 절)가
왜 CDC가 별도의 프로듀서 스레드와 유한 크기 큐를 두는지를 설명한다면, 여기서는
하나의 호출이 어떻게 로그 순방향 워크를 구동하고 — 각 분기마다 — 계속
진행할지, 멈출지, 잠들지, 죽을지를 결정하는지를 추적한다. Chapter 3에서는
request/state 배선을, Chapter 4에서는 next_extraction_lsa 시드를,
Chapter 6에서는 cdc_log_extract 호출을 다뤘다. 데몬 자체는 10 ms 루퍼를
갖지만, 이 함수는 내부에서 루프를 돌며 셧다운 때까지 반환하지 않는다 — 따라서
그 주기는 첫 번째 디스패치에만 의미가 있다.
5.1 진입, 세 가지 상태 필드, 루프 조건
섹션 제목: “5.1 진입, 세 가지 상태 필드, 루프 조건”루프를 지배하는 전역 변수는 세 개로, 모두 cdc_Gl.producer(CDC_PRODUCER
구조체, Chapter 1 표 참조)에 있다. 둘은 volatile로 선언되어 있는데, 다른
스레드들(세션/API 측, Chapter 8)이 이 값을 쓰고 이 스레드는 락 없이 폴링하기
때문이다.
| 필드 | 이 루프에서의 역할 | 존재 이유 |
|---|---|---|
state (volatile CDC_PRODUCER_STATE) | 이 스레드가 자신의 생존 상태를 공개한다: RUN, WAIT, DEAD. 다른 스레드가 이 값을 스핀 대기한다. | cdc_pause_producer/cdc_kill_producer가 데몬이 파킹되었거나 종료되었음을 확인할 수 있게 한다. |
request (volatile CDC_PRODUCER_REQUEST) | 이 스레드가 명령을 읽는다: TO_WAIT, TO_BE_DEAD, NONE. | API 측에서 오는 제어 채널. |
produced_queue_size (int) | 푸시됐으나 아직 확인되지 않은 페이로드의 누적 바이트 합계. | 큐 가득 찬 가드(5.3)를 구동하는 백프레셔 누산기. |
프롤로그는 스레드에 CDC 데몬 태그를 붙이고 RUN을 선언한다.
// cdc_loginfo_producer_execute -- src/transaction/log_manager.cTHREAD_ENTRY *thread_p = &thread_ref;thread_p->is_cdc_daemon = true; /* <- log subsystem knows reads come from CDC */cdc_Gl.producer.state = CDC_PRODUCER_STATE_RUN;while (cdc_Gl.producer.request != CDC_REQUEST_PRODUCER_TO_BE_DEAD) { // ... loop body ... }cdc_Gl.producer.state = CDC_PRODUCER_STATE_DEAD;조건식은 TO_BE_DEAD만 검사한다. TO_WAIT는 루프를 종료하지 않는다 —
이는 내부 분기(5.2)에서 파킹 후 continue하는 경로다. 유일한 출구는
컨트롤러가 request = TO_BE_DEAD를 설정하는 것뿐이다.
불변식 — state는 종료 시 정확히 한 번 DEAD에 도달해야 한다. 루프는
진입 시 state = RUN을 설정하고, 루프가 끝난 직후 state = DEAD를 설정한다.
cdc_kill_producer는 request = TO_BE_DEAD를 설정한 뒤 while (state != CDC_PRODUCER_STATE_DEAD)를 바쁜 대기(busy-wait)한다. state = DEAD를
건너뛰고 반환하는 경로(예: 이미 죽은 error: 레이블, 5.7)가 있으면 그 컨트롤러는
영원히 스핀하게 된다.
Figure 5-1은 한 번 반복의 분기 구조를 보여준다.
5.2 분기 A — wait_cond에서의 제어 일시 정지
섹션 제목: “5.2 분기 A — wait_cond에서의 제어 일시 정지”루프의 첫 번째 구문은 제어 채널을 읽는다.
// cdc_loginfo_producer_execute -- src/transaction/log_manager.cif (cdc_Gl.producer.request == CDC_REQUEST_PRODUCER_TO_WAIT) { cdc_Gl.producer.state = CDC_PRODUCER_STATE_WAIT; /* <- publish parked */
pthread_mutex_lock (&cdc_Gl.producer.lock); pthread_cond_wait (&cdc_Gl.producer.wait_cond, &cdc_Gl.producer.lock); pthread_mutex_unlock (&cdc_Gl.producer.lock);
cdc_Gl.producer.state = CDC_PRODUCER_STATE_RUN; /* <- republish live */ continue; /* <- re-test request */ }이것은 요청에 의한 일시 정지다(5.3의 큐 가득 참 일시 정지와 구별된다).
블로킹하기 전에 WAIT를 공개하므로, cdc_pause_producer의
while (state != CDC_PRODUCER_STATE_WAIT) 바쁜 대기가 파킹을 관측할 수 있다.
깨어난 뒤에는 “작업 재개”를 가정하는 대신 continue해서 루프 상단으로 돌아가
request를 재검사한다 — 사실상 셧다운이었던 wakeup도 이 경로로 깔끔하게
종료된다. lock/wait_cond 쌍은 cdc_loginfo_producer_daemon_init
(Chapter 3)에서 초기화된다.
이 코드는 교과서적인 방식으로 고전적인 lost-wakeup 창을 닫지 않는다는 점에
유의하라. 프로듀서는 request를 mutex 바깥에서 검사한 후, 락을 잡고
cond_wait한다. 그 두 단계 사이에 컨트롤러가 request를 설정하고 시그널을
보내면 그 시그널은 유실된다. 시그널러들(cdc_wakeup_producer,
cdc_kill_producer)은 cdc_Gl.producer.lock을 보유하지 않은 채로
pthread_cond_signal을 호출하므로, 락이 request-store와 wait를 직렬화하지
못한다. 이 설계는 레이스를 다른 방식으로 허용한다: cdc_kill_producer는
while (state != ...DEAD) { signal; sleep(1); } 루프로 재시그널하고,
cdc_pause_producer는 시그널을 아예 보내지 않고 request를 설정한 뒤
state를 스핀하며 기다린다 — 프로듀서가 다음 루프 상단에서 request를 다시
읽을 것을 신뢰한다. 시그널 하나가 유실되더라도 최악의 경우 루퍼 주기 한 번만
지연될 뿐, 영구 행업은 발생하지 않는다.
5.3 분기 B — 큐 가득 찬 백프레셔 일시 정지
섹션 제목: “5.3 분기 B — 큐 가득 찬 백프레셔 일시 정지”일시 정지 요청이 없으면, 루프는 OR로 연결된 두 가지 조건으로 유한 큐를 검사한다.
// cdc_loginfo_producer_execute -- src/transaction/log_manager.cif (cdc_Gl.producer.produced_queue_size >= MAX_CDC_LOGINFO_QUEUE_SIZE || cdc_Gl.loginfo_queue->is_full ()) { cdc_Gl.producer.state = CDC_PRODUCER_STATE_WAIT; cdc_pause_consumer (); /* <- ask consumer to drain & stop */ // ... lock / cond_wait / unlock as in Branch A, then state = RUN ... cdc_Gl.producer.produced_queue_size -= cdc_Gl.consumer.consumed_queue_size; cdc_Gl.consumer.consumed_queue_size = 0; /* <- reconcile counters */
cdc_wakeup_consumer (); continue; }두 조건은 서로 다른 것을 측정한다.
produced_queue_size >= MAX_CDC_LOGINFO_QUEUE_SIZE— 바이트 예산 (log_impl.h의 32 MB)으로, produce마다tmp->length를 누산한다(5.6). 거대한 행 이미지 몇 개가 있으면 이것이 먼저 발화한다.cdc_Gl.loginfo_queue->is_full()—lockfree::circular_queue<CDC_LOGINFO_ENTRY *>링의 슬롯 수 기반 한도. 작은 엔트리가 많으면 이것이 먼저 발화한다.
분기 A와 달리, 이 분기는 컨슈머도 일시 정지시키고(cdc_pause_consumer가
consumer.request = CDC_REQUEST_CONSUMER_TO_WAIT를 설정), 나중에 다시 깨워
드레인된 공간을 확보한다.
불변식 — produced_queue_size는 하나의 직렬화된 지점에서 조정된다.
프로듀서만 tmp->length를 더하고, 컨슈머만 자신의 consumed_queue_size를
더하며, 뺄셈은 오직 여기서만 일어난다 — 컨슈머가 일시 정지하여 안정된 수치를
보고한 뒤에만 실행된다. pause_consumer → cond_wait → reconcile → wakeup_consumer 순서가 이를 직렬화한다. 동시 뺄셈이 일어나면 카운터가
어긋나 프로듀서가 막히거나 큐가 넘칠 수 있다.
5.4 분기 C — 로그 꼬리 슬립
섹션 제목: “5.4 분기 C — 로그 꼬리 슬립”두 가드를 모두 통과하면, 루프는 어펜더의 다음 출력 LSA와 워크를 비교한다.
// cdc_loginfo_producer_execute -- src/transaction/log_manager.cnxio_lsa = log_Gl.append.get_nxio_lsa ();
pthread_mutex_lock (&cdc_Gl.producer.lock);
if (LSA_ISNULL (&cdc_Gl.producer.next_extraction_lsa) || LSA_GE (&cdc_Gl.producer.next_extraction_lsa, &nxio_lsa)) { /* LOG_HA_DUMMY_SERVER_STATUS is appended every 1 second and flushed. * So it is expected to be woken up by looper within period of looper */ pthread_mutex_unlock (&cdc_Gl.producer.lock); sleep (1); continue; }소스 주석은 레코드 이름을 LOG_HA_DUMMY_SERVER_STATUS로 잘못 표기했는데,
실제 enum은 LOG_DUMMY_HA_SERVER_STATE다. get_nxio_lsa는 어펜더의 next-i/o
LSA — 내구적으로 접근 가능한 로그의 상한선(high-water mark) — 를 반환한다.
이 분기는 next_extraction_lsa가 NULL(시드가 없거나 리셋된 경우 — Chapter 4)
이거나 >= nxio_lsa(워크가 꼬리를 따라잡음; 커서 너머에 완전히 기록된 레코드가
없음)일 때 발화한다. 그러면 프로듀서 락을 해제하고 sleep(1)한 뒤
continue한다. 1초 슬립은 LOG_DUMMY_HA_SERVER_STATE 하트비트에 연동되어
있는데, 이 레코드는 활성 서버에서 약 1초마다 플러시되므로, 슬립 후에는
nxio_lsa가 적어도 하트비트만큼 앞으로 전진한다 — 유저 쓰기가 없는 유휴
데이터베이스에서도 워크가 굶지 않는다.
불변식 — 프로듀서 락이 next_extraction_lsa의 찢어진 읽기(torn read)를
막는다. API 측이 이 LOG_LSA를 덮어쓸 수 있으므로, 여기(5.4)와 5.5에서
락을 보유한 채 읽는다. 슬립 경로에서 명시적으로 언락하는 것은 그 읽기를
원자적으로 유지하기 위해서다. 생략하면 모든 API 호출자가 데드락에 걸린다.
5.5 process_lsa ↔ next_extraction_lsa 핸드오프
섹션 제목: “5.5 process_lsa ↔ next_extraction_lsa 핸드오프”락을 보유한 상태에서 스크래치 엔트리를 리셋하고 커서를 동기화한다.
// cdc_loginfo_producer_execute -- src/transaction/log_manager.c// ... reset scratch log_info_entry: length=0, next_lsa=NULL, log_info=NULL ...if (LSA_ISNULL (&process_lsa)) { LSA_COPY (&process_lsa, &cdc_Gl.producer.next_extraction_lsa); /* first iter or after API reset */ }else { LSA_COPY (&cdc_Gl.producer.next_extraction_lsa, &process_lsa); /* steady state */ }
assert (!LSA_ISNULL (&process_lsa));LSA_COPY (&cur_log_rec_lsa, &process_lsa);
pthread_mutex_unlock (&cdc_Gl.producer.lock);process_lsa는 cdc_log_extract가 전진시키는 스레드-프라이빗 워크 커서
(Chapter 6)이고, next_extraction_lsa는 공유, API에 노출되는 커서다.
프라이빗 커서가 NULL이면(첫 번째 반복 또는 리셋 이후) 권위 있는 공유 시드에서
끌어오고, 그렇지 않으면 프라이빗 워크가 공유 커서에 밀어 넣는다. cur_log_rec_lsa는
추출이 process_lsa를 변경하기 전에 레코드 자체의 LSA를 스냅샷한다(나중에
last_loginfo_queue_lsa, 5.6). 비용이 큰 cdc_log_extract 이전에 락을 해제한다.
5.6 추출 호출, 오류 분류, produce
섹션 제목: “5.6 추출 호출, 오류 분류, produce”락을 해제한 상태에서 레코드 하나를 추출한다.
// cdc_loginfo_producer_execute -- src/transaction/log_manager.cerror = cdc_log_extract (thread_p, &process_lsa, &log_info_entry);if (!(error == NO_ERROR || error == ER_CDC_LOGINFO_ENTRY_GENERATED)) { cdc_log ("... cdc_log_extract() error(%d) ...", error, LSA_AS_ARGS (&cur_log_rec_lsa)); if (!CDC_IS_IGNORE_LOGINFO_ERROR (error)) { continue; /* <- non-ignorable hard error: retry from same cursor */ } /* else: ignorable -- fall through, treat like "nothing produced" */ }cdc_log_extract(Chapter 6)는 세 가지 결과 클래스 중 하나를 반환한다.
-
ER_CDC_LOGINFO_ENTRY_GENERATED— “페이로드가log_info_entry에 구성되었다”.!(... || error == ...GENERATED)검사가 이것을 오류 분기에서 제외한다. produce가 발생하는 유일한 클래스다. -
NO_ERROR— 레코드가 워크를 전진시켰지만 페이로드를 생성하지 않았다. 두 분기 모두 통과해 흘러내린다. -
그 외 모든 것 — 오류로 기록된 후
CDC_IS_IGNORE_LOGINFO_ERROR로 분류된다.// CDC_IS_IGNORE_LOGINFO_ERROR -- src/transaction/log_manager.c#define CDC_IS_IGNORE_LOGINFO_ERROR(ERROR) \(((ERROR) == ER_CDC_IGNORE_LOG_INFO) \|| ((ERROR) == ER_CDC_IGNORE_LOG_INFO_INTERNAL) \|| ((ERROR) == ER_CDC_IGNORE_TRANSACTION))무시 불가능한(non-ignorable) 오류는 커서를 전진시키지 않고
continue한다 — 데몬을 죽이지 않는 “현재 위치 재시도”다. 무시 가능한(ignorable) 오류 (필터링된 트랜잭션 또는 CDC가 건너뛰는 내부 전용 레코드)는if를 통과해 흘러내린다.cdc_log_extract가 이미process_lsa를 그 너머로 전진시켰으므로, 흘러내리는 것은 그냥 “produce 없음”을 의미한다.
produce 경로는 클래스 1에 대해서만 실행된다.
// cdc_loginfo_producer_execute -- src/transaction/log_manager.cif (error == ER_CDC_LOGINFO_ENTRY_GENERATED) { CDC_LOGINFO_ENTRY *tmp = (CDC_LOGINFO_ENTRY *) malloc (sizeof (CDC_LOGINFO_ENTRY)); if (tmp == NULL) { // ... er_set ER_OUT_OF_VIRTUAL_MEMORY ... error = ER_OUT_OF_VIRTUAL_MEMORY; continue; /* <- OOM: drop and retry */ }
tmp->length = log_info_entry.length; tmp->log_info = log_info_entry.log_info; /* <- transfer payload ownership */ LSA_COPY (&tmp->next_lsa, &process_lsa); /* <- now-advanced cursor */
if (cdc_Gl.is_queue_reinitialized) { free_and_init (tmp->log_info); free_and_init (tmp); cdc_Gl.is_queue_reinitialized = false; continue; /* <- mid-flight reset: discard */ }
cdc_Gl.loginfo_queue->produce (tmp); cdc_Gl.producer.produced_queue_size += tmp->length; LSA_COPY (&cdc_Gl.last_loginfo_queue_lsa, &cur_log_rec_lsa); }각 분기를 살펴보면:
- 힙 엔트리
malloc. 재사용되는 스택log_info_entry를 힙의CDC_LOGINFO_ENTRY로 복사한다.tmp->log_info는 추출된 버퍼의 소유권을 가져오고(포인터 재귀속),tmp->next_lsa는 전진된 커서를 기록한다. malloc실패.ER_OUT_OF_VIRTUAL_MEMORY를 설정하고continue한다.log_info_entry.log_info는 누수된다 — 알려진 거친 부분이다(OOM는 어차피 치명적).is_queue_reinitialized비행 중 드롭. 추출 중에 컨트롤러가 큐를 리셋했다면(Chapter 8), 해당 엔트리는 이전 세대에 속하므로, 분기가 둘 다 해제하고 플래그를 지운 뒤continue한다 — 리셋당 정확히 한 번, 플래그가 소비되는 유일한 지점이다.- produce.
produce(tmp)가 포인터를 큐에 넣는다.produced_queue_size += tmp->length가 바이트 예산 가드(5.3)를 공급하고,last_loginfo_queue_lsa는 인큐된 레코드 자체의 LSA를 기록한다(next_lsa가 아님).
불변식 — produce(tmp) 성공 이후 tmp는 컨슈머의 소유다.
produce 전의 모든 중단 경로는 tmp를 직접 해제한다. produce 후에 이를
해제하는 경로는 없다. 핸드오프는 락-프리 큐를 통한 raw 포인터이므로, 여기서
잘못 해제하면 컨슈머가 손상된다.
flowchart TD
A["루프 상단"] --> B{"request==TO_WAIT?"}
B -- yes --> A
B -- no --> C{"큐 가득 참?"}
C -- yes --> A
C -- no --> E{"꼬리: NULL 또는<br/>>= nxio_lsa?"}
E -- yes --> A
E -- no --> G["cdc_log_extract"]
G --> H{"오류 클래스"}
H -- "hard, non-ignorable" --> A
H -- "ignorable / NO_ERROR" --> A
H -- "GENERATED" --> K{"tmp NULL?"}
K -- yes --> A
K -- no --> L{"is_queue_reinitialized?"}
L -- yes --> A
L -- no --> M["produce; 장부 처리"]
M --> A
Figure 5-1. 루프 한 번 반복의 분기 골격. 각 결정과 continue/produce 결과는
5.2~5.6에서 추적된다. 유일한 출구(request == TO_BE_DEAD가 while 조건식을
실패시키는 경우)는 표시하지 않았다.
5.7 루프 종료와 두 개의 후미 레이블
섹션 제목: “5.7 루프 종료와 두 개의 후미 레이블”request가 TO_BE_DEAD가 되면 while 조건이 실패하고 제어가 빠져나온다.
// cdc_loginfo_producer_execute -- src/transaction/log_manager.ccdc_Gl.producer.state = CDC_PRODUCER_STATE_DEAD; /* releases cdc_kill_producer */end: thread_p->is_cdc_daemon = false; return;error: /* <- bypasses the state = DEAD above */ thread_p->is_cdc_daemon = false; return;루프 후 state = DEAD는 cdc_kill_producer에서 스핀 중인 컨트롤러를 해제한다.
그 다음 is_cdc_daemon을 해제하고 반환한다. end:와 error: 레이블은 도달
불가능하다 — 어떤 goto도 이를 대상으로 하지 않는다(오류 처리는 continue를
사용한다). 수정자를 위한 메모: 미래에 goto error를 추가하면 state = DEAD를
건너뛰어 5.1의 생존 불변식을 위반하고, 종료 중인 컨트롤러를 영구히 멈추게
만든다.
5.8 챕터 요약 — 핵심 정리
섹션 제목: “5.8 챕터 요약 — 핵심 정리”- 하나의 함수, 하나의 장수 루프.
request == TO_BE_DEAD가 될 때까지 스레드를 소유한다.TO_WAIT는 내부 파킹-후-continue 분기로, 절대 출구가 아니다.state는 반드시 정확히 한 번DEAD에 도달해야 한다. - 두 가지 일시 정지가
wait_cond를 공유한다. 분기 A(TO_WAIT)는 컨트롤러가 해결한다. 분기 B(큐 가득 참)는 컨슈머도 일시 정지시키고, 깨어난 후produced_queue_size -= consumed_queue_size를 조정한다 — 바이트 예산에 대한 유일한 뺄셈 지점이다. - 큐 가득 찬 가드에는 OR로 연결된 두 개의 트리거가 있다. 32 MB 바이트
예산(
MAX_CDC_LOGINFO_QUEUE_SIZE)과 슬롯 수 한도(loginfo_queue->is_full()). 거대한 페이로드는 전자를, 작은 것들은 후자를 먼저 발화한다. - 꼬리 가드는 HA 하트비트에 동기화된 1초 슬립을 한다.
next_extraction_lsa가 NULL이거나>= nxio_lsa이면 루프가 1초 잠들고, 그 동안LOG_DUMMY_HA_SERVER_STATE하트비트가 꼬리를 전진시킨다 — 유저 쓰기가 전혀 없어도 전방 진행이 보장된다. - 프로듀서 락 아래의 두 커서. 프라이빗
process_lsa가 NULL이면 공유next_extraction_lsa에서 끌어오고, 그렇지 않으면 공유 커서로 밀어 넣는다. 락은 찢어진 읽기를 막으며, 추출 이전에 해제된다. ER_CDC_LOGINFO_ENTRY_GENERATED가 produce 계약이다. 오직 이 결과만이 힙CDC_LOGINFO_ENTRY생성,produce호출,produced_queue_size/last_loginfo_queue_lsa장부 처리를 유발한다.NO_ERROR와 무시 가능한 오류는 워크만 전진시키고, 하드 오류는 데몬을 죽이지 않고continue한다.produce이후 엔트리는 컨슈머의 소유다(produce 전 중단 경로는tmp를 직접 해제한다). 죽은end:/error:레이블은 도달 불가능하다.
Chapter 6: 레코드별 디스패치와 단일 변경 재구성
섹션 제목: “Chapter 6: 레코드별 디스패치와 단일 변경 재구성”프로듀서 루프(Chapter 5)는 레코드 단위로 cdc_log_extract를 호출한다.
이 챕터는 커서가 가리키는 단일 로그 레코드를 두고 CDC가 이벤트 종류를 어떻게
판별하고 인메모리 CDC_LOGINFO_ENTRY를 어떻게 구성하는지를 설명한다.
페이로드 실체화(cdc_get_recdes / cdc_make_dml_loginfo)는 Chapter 7에서
다룬다. 보충 로깅의 개념 모델과 네 가지 이벤트 클래스는 companion 문서
cubrid-cdc.md의 “LOG_SUPPLEMENTAL_INFO” 절 및 “cdc_make_loginfo” 절을
참고한다.
6.1 이 코드가 읽는 두 개의 레코드 헤더
섹션 제목: “6.1 이 코드가 읽는 두 개의 레코드 헤더”cdc_log_extract는 레코드 타입을 위한 고정 헤더를 먼저 읽고, 보충 레코드인
경우에 한해 supplement 서브타입을 나타내는 두 번째 헤더를 추가로 읽는다.
LOG_RECORD_HEADER(log_record.hpp)는 모든 레코드에 공통으로 붙는 접두
구조체로, LOG_GET_LOG_RECORD_HEADER(즉, log_page_p->area + lsa->offset
위치의 캐스트)로 읽는다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
prev_tranlsa | 동일 트랜잭션의 이전 레코드 LSA | 역방향 undo 체인; CDC는 사용하지 않음 |
back_lsa | 역방향 로그 주소(이전 물리 레코드) | 물리적 체이닝; CDC는 사용하지 않음 |
forw_lsa | 순방향 로그 주소(다음 물리 레코드) | next_log_rec_lsa로 복사됨; LSA_ISNULL이면 “로그 없음” 센티넬 |
trid | 이 레코드를 기록한 트랜잭션 식별자 | tran_user / tran_ignore 조회 키 |
type | LOG_RECTYPE 판별자(commit, abort, supplemental, …) | 외부 switch 분기 기준 |
LOG_REC_SUPPLEMENT(log_record.hpp)는 LOG_SUPPLEMENTAL_INFO 타입일 때만
헤더 다음에 위치한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
rec_type | SUPPLEMENT_REC_TYPE 판별자(TRAN_USER, DDL, INSERT, UPDATE, DELETE, TRIGGER_*) | 내부 switch 분기 기준 |
length | supplement 페이로드의 바이트 길이 | 버퍼 크기 산정; cdc_log_extract는 복구된 RECDES를 신뢰하므로 이 값을 재계산한다 |
불변 조건 — supplement 페이로드는 rec_type에 따라 배치가 결정된다.
INSERT/UPDATE/DELETE의 경우 페이로드는 CLASS OID | UNDO LSA | REDO LSA 순서이다
(DELETE는 REDO를 생략하고, INSERT는 UNDO를 생략한다). cdc_log_extract는
offset 0에서 classoid를, 고정 offset에서 LSA를 읽는데, 이는 프로듀서(Chapter 2)가
이 순서로 기록했기 때문에만 올바르다. 양측이 어긋나면 CDC는 잘못된 OID를 읽어
필터링을 조용히 오판한다.
graph LR
subgraph page["process_lsa 위치의 로그 페이지"]
H["LOG_RECORD_HEADER<br/>type, trid, forw_lsa"]
S["LOG_REC_SUPPLEMENT<br/>rec_type, length"]
P["페이로드<br/>OID | undo_lsa | redo_lsa"]
end
H -->|"type==SUPPLEMENTAL_INFO 인 경우만"| S
S -->|"cdc_get_undo_record를 통해 복구"| P
Figure 6-1. 보충 레코드의 헤더 체인.
6.2 페이지 fetch와 temp_logbuf 이중 버퍼
섹션 제목: “6.2 페이지 fetch와 temp_logbuf 이중 버퍼”process_lsa를 cur_log_rec_lsa에 복사한 뒤, 함수는
CDC_GET_TEMP_LOGPAGE로 페이지를 fetch하고 tmpbuf_index = process_lsa->pageid % 2를 초기화한다. 프로듀서는 두 슬롯짜리 캐시
temp_logbuf[2](CDC_TEMP_LOGBUF, log_impl.h)를 소유하며, 각 슬롯은
호출 사이에 살아남는 백킹 버퍼를 가리키는 log_page_p를 담는다.
CDC_GET_TEMP_LOGPAGE는 미스 발생 시 fetch를 수행하는 조회 매크로다.
// CDC_GET_TEMP_LOGPAGE -- src/transaction/log_impl.hif (cdc_Gl.producer.temp_logbuf[(process_lsa)->pageid % 2].log_page_p->hdr.logical_pageid != (process_lsa)->pageid) { /* <- 미스: 슬롯에 다른 페이지 있음 */ if (logpb_fetch_page (...) != NO_ERROR) { goto error; } /* <- error: 레이블이 존재하는 이유 */ memcpy (cdc_Gl.producer.temp_logbuf[...].log_page_p, (log_page_p), IO_MAX_PAGE_SIZE); }else (log_page_p) = cdc_Gl.producer.temp_logbuf[(process_lsa)->pageid % 2].log_page_p; /* <- 히트 */한 레코드가 페이지 경계를 걸칠 수 있다. 커서가 경계를 넘으면
(LOG_READ_ADVANCE_WHEN_DOESNT_FIT) log_page_p는 페이지 N+1을 가리키지만,
N은 여전히 필요할 수 있다. 패리티 인덱싱은 N을 슬롯 N%2에, N+1을
(N+1)%2에 유지한다. 나머지 두 매크로도 CDC_GET과 대칭적으로 동작한다.
CDC_CHECK_TEMP_LOGPAGE는 process_lsa->pageid % 2 != *tmpbuf_index일 때
실행되어 *tmpbuf_index를 뒤집고, 새로 진입한 페이지를 해당 슬롯에 memcpy한다.
CDC_UPDATE_TEMP_LOGPAGE는 반대로 — 요청된 페이지가 이미 캐시에 있을 때도 강제로
재fetch(logpb_fetch_page, 실패 시 goto error, 성공 후 memcpy)한다. 로그 끝
조기 종료와 supplement 복사 이후 디스크 동기화를 위해 사용된다.
불변 조건 — tmpbuf_index는 항상 process_lsa->pageid % 2와 같다.
초기 CDC_GET_TEMP_LOGPAGE 이후 시드되며, 이후 모든 포스트-어드밴스
CDC_CHECK_TEMP_LOGPAGE가 유지한다. 어드밴스 후 CHECK를 빠뜨린 분기가
있으면, 이후 memcpy가 아직 필요한 페이지를 덮어쓸 수 있다.
6.3 로그 끝 조기 종료
섹션 제목: “6.3 로그 끝 조기 종료”헤더를 읽은 직후, switch 이전에:
// cdc_log_extract -- src/transaction/log_manager.cif (log_rec_header->type == LOG_END_OF_LOG || LSA_ISNULL (&log_rec_header->forw_lsa)) { CDC_UPDATE_TEMP_LOGPAGE (thread_p, process_lsa, log_page_p); /* <- 슬롯을 디스크와 동기화 */ error = ER_CDC_NULL_EXTRACTION_LSA; goto error; } /* <- 커서 되감기, 6.8 참조 */log_type = log_rec_header->type; trid = log_rec_header->trid;LSA_COPY (&next_log_rec_lsa, &log_rec_header->forw_lsa); /* <- 성공 시 커서가 여기로 이동 */// ... LOG_READ_ADD_ALIGN으로 헤더 지나침 ...LOG_END_OF_LOG 또는 null forw_lsa는 CDC가 테일에 도달했음을 의미한다.
ER_CDC_NULL_EXTRACTION_LSA(-1293)를 반환하고 error:(6.8)를 거쳐
cur_log_rec_lsa로 되감으므로, 다음 반복은 새 로그가 도착할 때까지 같은
LSA를 재시도한다(companion 문서 §“CDC producer”의 “테일 대기” 동작). 그렇지
않으면 log_type, trid, next_log_rec_lsa를 캡처하고 헤더를 지나친다.
6.4 LOG_RECTYPE에 대한 외부 switch
섹션 제목: “6.4 LOG_RECTYPE에 대한 외부 switch”flowchart TD
A["LOG_RECORD_HEADER 읽기"] --> B{"로그 끝 또는 forw_lsa null"}
B -->|예| EOL["ER_CDC_NULL_EXTRACTION_LSA -> error"]
B -->|아니오| SW{"switch log_type"}
SW -->|COMMIT / ABORT| DCL["6.5 DCL 분기"]
SW -->|DUMMY_HA_SERVER_STATE| TMR["6.6 Timer 분기"]
SW -->|SUPPLEMENTAL_INFO| SUP["6.7 Supplement 분기"]
SW -->|default| END["fall through to end"]
Figure 6-2. 세 가지 레코드 타입만 이벤트를 생성한다. 그 외 모든
LOG_RECTYPE은 default: break에 걸린다(커서는 next_log_rec_lsa로
전진).
6.5 LOG_COMMIT / LOG_ABORT — DCL 이벤트 생성
섹션 제목: “6.5 LOG_COMMIT / LOG_ABORT — DCL 이벤트 생성”두 타입은 하나의 분기를 공유한다. LOG_REC_DONETIME을 지나 전진하고,
재동기화한 뒤, 네 개의 게이트를 통과한다.
// cdc_log_extract -- src/transaction/log_manager.c (LOG_COMMIT / LOG_ABORT)// ... donetime 지나 전진, CDC_CHECK_TEMP_LOGPAGE ...if (cdc_Gl.producer.tran_ignore.count (trid) != 0) /* 게이트 1: 독성 trid */ { cdc_Gl.producer.tran_ignore.erase (trid); break; } /* <- 기억 삭제; 할당된 것 없음 */donetime = (LOG_REC_DONETIME *) (log_page_p->area + process_lsa->offset);if (cdc_Gl.producer.tran_user.count (trid) == 0) goto end; /* 게이트 2: user 없음 -> 무해하게 스킵 */else tran_user = cdc_Gl.producer.tran_user.at (trid);if (!cdc_is_filtered_user (tran_user)) break; /* 게이트 3: 추출 대상 user 아님 */if ((error = cdc_make_dcl_loginfo (donetime->at_time, trid, tran_user, log_type, log_info_entry)) != ER_CDC_LOGINFO_ENTRY_GENERATED) goto error; /* 게이트 4 */free_and_init (tran_user); cdc_Gl.producer.tran_user.erase (trid); break; /* <- 트랜잭션 완료 */게이트 1은 6.7의 abort 폴백이 삽입한 ignore 마커를 지운다. 게이트 4는
센티넬을 성공으로 처리하고(6.13), 다른 값은 error:로 이동한다. 성공
시 캐시된 user를 break 전에 해제·삭제하므로 end:는 그것을 보지 못한다.
6.6 LOG_DUMMY_HA_SERVER_STATE — Timer 이벤트 생성
섹션 제목: “6.6 LOG_DUMMY_HA_SERVER_STATE — Timer 이벤트 생성”// cdc_log_extract -- src/transaction/log_manager.c (LOG_DUMMY_HA_SERVER_STATE)// ... ha_dummy 지나 전진, CDC_CHECK_TEMP_LOGPAGE, ha_dummy 캐스트 ...if ((error = cdc_make_timer_loginfo (ha_dummy->at_time, trid, NULL, log_info_entry)) == ER_OUT_OF_VIRTUAL_MEMORY) goto error;break;HA dummy의 at_time 하트비트가 NULL user를 가진 CDC_TIMER 이벤트가 된다.
ER_OUT_OF_VIRTUAL_MEMORY만 error:로 분기하며, 성공 반환값
ER_CDC_LOGINFO_ENTRY_GENERATED는 error에 대입되지만 별도로 처리되지
않아 break로 end:에 떨어지고 구성된 엔트리를 유지한다.
6.7 LOG_SUPPLEMENTAL_INFO — 재구성의 핵심
섹션 제목: “6.7 LOG_SUPPLEMENTAL_INFO — 재구성의 핵심”DML 이벤트와 DDL 이벤트를 생성한다. 다섯 단계로 구성된다.
Stage 1 — 전진, 재동기화, ignore 체크.
// cdc_log_extract -- src/transaction/log_manager.c (LOG_SUPPLEMENTAL_INFO)// ... supplement 지나 전진, CDC_CHECK_TEMP_LOGPAGE ...if (cdc_Gl.producer.tran_ignore.count (trid) != 0) goto end; /* <- trid가 이미 복구 불가 */Stage 2 — supplement 헤더 읽기, 페이로드 복구.
supplement = (LOG_REC_SUPPLEMENT *) (log_page_p->area + process_lsa->offset);rec_type = supplement->rec_type; /* <- 내부 switch 판별자 */// ... LOG_READ_ADD_ALIGN으로 supplement 지나침, CDC_CHECK_TEMP_LOGPAGE ...if (cdc_get_undo_record (thread_p, log_page_p, cur_log_rec_lsa, &supp_recdes) != S_SUCCESS) { error = ER_FAILED; goto error; }supplement_length = sizeof (supp_recdes.type) + supp_recdes.length; /* <- 재계산, 신뢰 안 함 */// ... supplement_data malloc (NULL -> ER_OUT_OF_VIRTUAL_MEMORY, goto error); type+data memcpy;// free_and_init (supp_recdes.data) ...CDC_UPDATE_TEMP_LOGPAGE (thread_p, process_lsa, log_page_p); /* <- 슬롯을 디스크로 강제 복구 */cdc_get_undo_record(Chapter 7)는 원시 레코드를 supp_recdes에 복구한다.
복구된 RECDES가 권위 있는 정보이므로 헤더의 length는 덮어쓰인다.
Stage 3 — 트랜잭션 user 해석 (TRAN_USER는 건너뜀).
if (rec_type != LOG_SUPPLEMENT_TRAN_USER) { if (cdc_Gl.producer.tran_user.count (trid) == 0) { if ((error = cdc_find_user (thread_p, cur_log_rec_lsa, trid, &tran_user)) == NO_ERROR) cdc_Gl.producer.tran_user.insert (std::make_pair (trid, tran_user)); /* <- 캐시 저장 */ else if (error == ER_CDC_IGNORE_TRANSACTION) { cdc_Gl.producer.tran_ignore.insert (std::make_pair (trid, 1)); goto end; } /* <- 독성 삽입 */ else goto end; } /* <- cdc_find_user가 다른 이유로 실패: 레코드 스킵 */ else tran_user = cdc_Gl.producer.tran_user.at (trid); /* <- 이미 캐시됨 */ if (!cdc_is_filtered_user (tran_user)) goto end; } /* <- user가 추출 대상 아님 */TRAN_USER는 user를 설정하는 레코드이므로 기존 user를 필요로 하지 않는다.
그 외 모든 타입은 캐시 미스 시 cdc_find_user(6.11)를 호출한다. 이 함수가
ER_CDC_IGNORE_TRANSACTION을 반환하면(LOG_ABORT를 스캔한 경우) tran_ignore
독성 엔트리가 삽입되어, commit/abort가 마커를 삭제할 때까지(6.5 게이트 1)
해당 trid의 이후 레코드는 Stage 1에서 모두 버려진다. 이후 user 필터가
요청되지 않은 user를 제거한다.
Stage 4 — rec_type에 대한 내부 switch (Figure 6-3). 각 분기는
CDC_DML_TYPE(CDC_INSERT=0, CDC_UPDATE, CDC_DELETE, CDC_TRIGGER_INSERT, CDC_TRIGGER_UPDATE, CDC_TRIGGER_DELETE)으로 대응된다.
flowchart TD
R{"switch rec_type"}
R -->|TRAN_USER| TU["페이로드에서 user 캐시\n이미 캐시된 경우 break"]
R -->|INSERT / TRIGGER_INSERT| INS["클래스 게이트 -> redo recdes\n-> CDC_INSERT / CDC_TRIGGER_INSERT"]
R -->|UPDATE / TRIGGER_UPDATE| UPD["클래스 게이트 -> undo+redo\nREC_ASSIGN_ADDRESS -> TRIGGER_INSERT 재타입"]
R -->|DELETE / TRIGGER_DELETE| DEL["클래스 게이트 -> undo recdes\n-> CDC_DELETE / CDC_TRIGGER_DELETE"]
R -->|DDL| DDL["cdc_make_ddl_loginfo"]
R -->|default| D["break"]
Figure 6-3. supplement 내부 디스패치.
모든 DML 분기는 클래스 게이트(Stage 5)로 시작하며,
ER_CDC_LOGINFO_ENTRY_GENERATED 이외의 값이 나오면 error:로 이동한다.
- TRAN_USER — 클래스 게이트 없음. 이미 캐시된 경우
break(시작과 끝 양쪽에서 로그). 그렇지 않으면supplement_length + 1을 malloc하고, 복사 후 NUL 종료하여tran_user에 삽입. 페이로드에서 직접 user 맵을 기록하는 유일한 분기. - INSERT / TRIGGER_INSERT — 게이트, offset
sizeof(OID)위치에서redo_lsa읽기, redo RECDES fetch,CDC_INSERT/CDC_TRIGGER_INSERT로 매핑. - UPDATE / TRIGGER_UPDATE — 게이트, undo+redo fetch. 특이 케이스:
undo_recdes.type == REC_ASSIGN_ADDRESS는 빈 OID 예약 슬롯으로,CDC_TRIGGER_INSERT로 재타입된다(rec_type이TRIGGER_UPDATE였음을assert로 검증); 그 외에는CDC_UPDATE/CDC_TRIGGER_UPDATE.ER_CDC_IGNORE_LOG_INFO/_INTERNAL은goto end로 허용. - DELETE / TRIGGER_DELETE — 게이트, undo RECDES만 fetch(삭제된 사전 이미지),
CDC_DELETE/CDC_TRIGGER_DELETE로 매핑. - DDL —
cdc_make_ddl_loginfo;ER_CDC_IGNORE_LOG_INFO->end, 센티넬 이외의 음수 ->error:. - default —
break.
Stage 5 — 클래스 게이트 (한 번만 인용; 세 DML 분기 모두 동일):
// cdc_log_extract -- src/transaction/log_manager.cmemcpy (&classoid, supplement_data, sizeof (OID));if (!cdc_is_filtered_class (classoid) || oid_is_system_class (&classoid)) { error = ER_CDC_IGNORE_LOG_INFO; goto end; }버리는 이유는 두 가지다. 클래스가 추출 대상이 아닌 경우
(!cdc_is_filtered_class), 또는 시스템 카탈로그 클래스인 경우
(oid_is_system_class — _db_class, _db_attribute, …). 둘 다
goto end로 이동하여 supplement_data를 해제한다.
6.8 공유 end: 및 error: 출구
섹션 제목: “6.8 공유 end: 및 error: 출구”두 레이블 모두 동일한 세 개의 NULL 가드 힙 소유자 — supplement_data,
undo_recdes.data, redo_recdes.data — 를 해제하며, 커서 처리만 다르다.
end: ...free... LSA_COPY (process_lsa, &next_log_rec_lsa); return error; /* <- 전진 */error: ...free... LSA_COPY (process_lsa, &cur_log_rec_lsa); return error; /* <- 되감기 */불변 조건 — end:는 전진을 확정하고, error:는 되감는다. 스킵-비실패
(goto end)는 process_lsa를 next_log_rec_lsa로 남기고, 실제 실패
(goto error)는 cur_log_rec_lsa로 복원하여 루프가 같은 레코드를
재시도하게 한다. 이로 인해 ER_CDC_NULL_EXTRACTION_LSA(6.3)는 안전한
테일 재시도가 된다. 일시적 실패를 end:로 잘못 라우팅하면 해당 레코드를
영구적으로 건너뛰게 된다.
6.9 cdc_make_dcl_loginfo — commit/abort 엔트리 패킹
섹션 제목: “6.9 cdc_make_dcl_loginfo — commit/abort 엔트리 패킹”// cdc_make_dcl_loginfo -- src/transaction/log_manager.cswitch (log_type) { case LOG_COMMIT: dcl_type = CDC_COMMIT; break; case LOG_ABORT: dcl_type = CDC_ABORT; break; default: assert (false); return ER_FAILED; } /* <- commit/abort만 허용 */length = OR_INT_SIZE + OR_INT_SIZE + or_packed_string_length (user, NULL) + OR_INT_SIZE + OR_INT_SIZE + OR_BIGINT_SIZE;loginfo_buf = (char *) malloc (length * 2 + MAX_ALIGNMENT);if (loginfo_buf == NULL) { error_code = ER_OUT_OF_VIRTUAL_MEMORY; goto error; }ptr = start_ptr = PTR_ALIGN (loginfo_buf, MAX_ALIGNMENT);ptr = or_pack_int (ptr, dcl_entry->length); /* 플레이스홀더 length, 아래에서 역기입 */// ... or_pack_int trid; or_pack_string user; or_pack_int dataitem_type(CDC_DCL);// or_pack_int dcl_type; or_pack_int64 at_time ...dcl_entry->length = ptr - start_ptr;or_pack_int (start_ptr, dcl_entry->length); /* <- 선행 length 역기입 */dcl_entry->log_info = (char *) malloc (dcl_entry->length); /* OOM -> error */memcpy (dcl_entry->log_info, start_ptr, dcl_entry->length);return ER_CDC_LOGINFO_ENTRY_GENERATED; /* <- 성공을 에러로 표현한 센티넬 */스크래치 버퍼 loginfo_buf(반환 전 해제)에 패킹한 뒤, 정확한 길이의 결과를
dcl_entry->log_info에 복사한다. 선행 length는 두 번 기록된다(플레이스홀더,
이후 역기입). malloc 실패 시 error:로 분기해 ER_OUT_OF_VIRTUAL_MEMORY 반환.
6.10 cdc_make_timer_loginfo — 하트비트 엔트리 패킹
섹션 제목: “6.10 cdc_make_timer_loginfo — 하트비트 엔트리 패킹”구조는 6.9와 동일하며 두 가지만 다르다. dataitem_type = CDC_TIMER이고,
dcl_type 필드가 없으므로 pack 시퀀스와 length 산정에서 각각 or_pack_int
/ OR_INT_SIZE 하나씩 줄어든다. user는 항상 NULL이며,
or_packed_string_length / or_pack_string은 이를 빈 문자열로 처리한다.
역기입 방식, ER_CDC_LOGINFO_ENTRY_GENERATED 반환, OOM error: 모두 동일.
6.11 cdc_find_user — 트랜잭션 user를 위한 전방 스캔
섹션 제목: “6.11 cdc_find_user — 트랜잭션 user를 위한 전방 스캔”트랜잭션 시작 TRAN_USER 레코드가 잘려 나간 경우, user는 commit 직전에
기록된 TRAN_USER에서 복구된다. cdc_find_user는 process_lsa부터 nxio_lsa
범위 안에서 전방으로 스캔한다(커밋된 트랜잭션의 TRAN_USER는 다음 flush될 LSA
이전에 디스크에 있다).
// cdc_find_user -- src/transaction/log_manager.cLOG_LSA nxio_lsa = log_Gl.append.get_nxio_lsa ();if (logpb_fetch_page (...) != NO_ERROR) { logpb_fatal_error (...); return ER_FAILED; }while (!LSA_ISNULL (&process_lsa) && LSA_LT (&process_lsa, &nxio_lsa)) { log_rec_hdr = LOG_GET_LOG_RECORD_HEADER (log_page_p, &process_lsa); LSA_COPY (&forw_lsa, &log_rec_hdr->forw_lsa); // ... LOG_READ_ADD_ALIGN으로 헤더 지나침 ... if (log_rec_hdr->type == LOG_SUPPLEMENTAL_INFO && log_rec_hdr->trid == trid) { // ... supplement 지나 전진; supplement 캐스트 ... if (supplement->rec_type == LOG_SUPPLEMENT_TRAN_USER) { *user = (char *) malloc (supplement->length + 1); /* OOM -> ER_OUT_OF_VIRTUAL_MEMORY */ // ... NULL 체크; ADD_ALIGN; 페이로드 memcpy; NUL 종료 ... return NO_ERROR; } } /* <- 발견 */ else if (log_rec_hdr->type == LOG_ABORT && log_rec_hdr->trid == trid) { er_set (..., ER_CDC_IGNORE_TRANSACTION, 1, trid); return ER_CDC_IGNORE_TRANSACTION; } assert (!(log_rec_hdr->type == LOG_COMMIT && log_rec_hdr->trid == trid)); /* user는 commit 앞에 옴 */ // ... forw_lsa가 페이지를 넘으면 재fetch (실패 시 fatal-error + ER_FAILED) ... LSA_COPY (&process_lsa, &forw_lsa); }return ER_FAILED; /* <- 스캔 소진 */다섯 가지 종료 경로가 있다. TRAN_USER 발견(NO_ERROR); 해당 malloc
OOM(ER_OUT_OF_VIRTUAL_MEMORY, 인라인으로 표시); 이 trid의 LOG_ABORT
(ER_CDC_IGNORE_TRANSACTION, cdc_log_extract가 tran_ignore 독성으로 변환);
fetch 실패(ER_FAILED, 초기 fetch 또는 루프 내 페이지 넘김 재fetch);
루프 소진(ER_FAILED). assert는 커밋된 트랜잭션이 commit 레코드 이전에
항상 user를 기록한다는 불변 조건을 코드화한다. 이것이 발화하면 프로듀서 측
보충 로깅(Chapter 2)이 깨진 것이다.
6.12 cdc_is_filtered_class와 cdc_is_filtered_user
섹션 제목: “6.12 cdc_is_filtered_class와 cdc_is_filtered_user”두 함수 모두 선형 멤버십 테스트이며, 빈 집합은 “전체 허용”을 뜻하는 관례를 따른다.
// cdc_is_filtered_class -- src/transaction/log_manager.cif (cdc_Gl.producer.num_extraction_class == 0) { return true; } /* <- 필터 없음 = 전부 허용 */for (i = 0; i < cdc_Gl.producer.num_extraction_class; i++) if (cdc_Gl.producer.extraction_classoids[i] == b_classoid) return true;return false;cdc_is_filtered_class는 OID를 uint64_t로 재해석하여 extraction_classoids[]와
비교한다. cdc_is_filtered_user도 동일한 구조로 extraction_user[]를 strcmp로
비교한다. 이름이 직관과 반대로 읽힌다는 점에 주의. true는 필터를 통과함을
의미하며, 빈 리스트는 “전부 추출”을 뜻한다.
6.13 ER_CDC_LOGINFO_ENTRY_GENERATED — 성공을 에러로 표현하는 관례
섹션 제목: “6.13 ER_CDC_LOGINFO_ENTRY_GENERATED — 성공을 에러로 표현하는 관례”cdc_make_* 빌더들의 “성공” 반환값은 ER_CDC_LOGINFO_ENTRY_GENERATED(-1294)로,
에러 코드 공간의 음수 값이다. 정수 하나로 세 가지 결과를 구분할 수 있다.
NO_ERROR(생성된 것 없음, 계속 스캔), 실제 음수 에러(중단), 그리고 센티넬(엔트리
구성됨). ER_CDC_IGNORE_LOG_INFO(-1295)와 ER_CDC_IGNORE_LOG_INFO_INTERNAL
(-1288)은 “의도적으로 아무것도 생성하지 않음”을 의미하며 end:로 라우팅된다.
성공 시 NO_ERROR를 반환하면 cdc_log_extract가 성공적으로 구성된 엔트리를
되감게 되므로, 빌더를 수정할 때는 반드시 이 계약을 유지해야 한다.
6.14 챕터 요약 — 핵심 정리
섹션 제목: “6.14 챕터 요약 — 핵심 정리”- 레코드 하나, switch 하나.
cdc_log_extract는LOG_RECORD_HEADER.type으로 분기한다.LOG_COMMIT/LOG_ABORT(DCL),LOG_DUMMY_HA_SERVER_STATE(Timer),LOG_SUPPLEMENTAL_INFO(DML/DDL) 세 타입만 이벤트를 생성한다. temp_logbuf[2]는 패리티 인덱싱된다. 경계를 걸치는 레코드가 아직 필요한 페이지를 덮어쓰지 않도록tmpbuf_index가process_lsa->pageid % 2를 추적한다.- User 해석은 지연되고 자가 복구된다.
tran_user를 먼저 읽고, 없으면cdc_find_user로 폴백하며, abort된 트랜잭션은 commit/abort가 삭제할 때까지tran_ignore독성으로 남는다. - 두 개의 필터 게이트 —
cdc_is_filtered_user와 클래스 게이트 (cdc_is_filtered_class+oid_is_system_class); 빈 추출 목록은 “전부 허용”. end:는 전진,error:는 되감기 — 실제 실패는cur_log_rec_lsa를 복원하여 같은 레코드를 재시도하게 한다.- 빌더는 성공을 음수 에러 코드로 신호한다.
ER_CDC_LOGINFO_ENTRY_GENERATED;ER_CDC_IGNORE_LOG_INFO*는 “의도적으로 아무것도 생성 안 함”을 의미하며end:로 라우팅된다. - 페이로드 실체화는 Chapter 7에서 — 이 분기는
cdc_get_recdes/cdc_make_dml_loginfo호출 전까지만 다룬다.
Chapter 7: 로우 이미지 구체화와 이벤트 페이로드 패킹
섹션 제목: “Chapter 7: 로우 이미지 구체화와 이벤트 페이로드 패킹”Chapter 6은 undo_lsa(이전)와 redo_lsa(이후)라는 두 LSA만 담은 DML 보조
레코드가 디스패치된 시점에서 끝났다. 이 LSA들만으로 프로듀서는 실제 로우 이미지를
어떻게 가져오고, 클라이언트가 디코딩할 수 있는 바이트 블롭으로 직렬화하는가? 두
단계로 나뉜다: LSA에서 RECDES까지 이어지는 간접 추적(cdc_get_recdes 외),
그리고 두 RECDES에서 CDC_LOGINFO_ENTRY.log_info까지의 패킹
(cdc_make_dml_loginfo 외). 와이어 포맷의 클라이언트측 디코딩은 Chapter 8에서,
DML/DDL/오류 분류 체계는 cubrid-cdc.md에서 다룬다.
7.1 이 챕터에서 다루는 세 구조체
섹션 제목: “7.1 이 챕터에서 다루는 세 구조체”flowchart LR RL["redo_lsa / undo_lsa\n(LOG_LSA)"] --> RD["RECDES\n(로우 이미지)"] OV["OVF_PAGE_LIST\n(페이지별 프래그먼트, 역방향 연결)"] --> RD ZIP["LOG_ZIP\n(호출별 압축 해제 스크래치)"] --> RD RD --> ENT["CDC_LOGINFO_ENTRY\n.log_info 바이트 블롭"]
Figure 7-1 — 데이터 흐름: LSA가 RECDES로 해소된다 (다중 페이지 로우는
OVF_PAGE_LIST 체인, 압축 데이터는 LOG_ZIP 경유); RECDES 쌍은 log_info로
패킹된다.
RECDES — 구체화된 레코드 디스크립터 (storage_common.h):
// struct recdes -- src/storage/storage_common.hstruct recdes{ int area_size; /* allocated capacity of data; negative == "peeked" */ int length; /* used length, excluding length/type fields */ INT16 type; /* REC_HOME, REC_NEWHOME, ... */ char *data; /* the record payload */};| 필드 | 역할 / 이유 |
|---|---|
area_size | data의 할당 용량; S_DOESNT_FIT 발생 시 확장되며, CDC의 malloc 경로에서는 항상 양수. |
length | 사용 중인 바이트 수; S_DOESNT_FIT일 때 음수로 반환되어 재할당 크기를 알려 준다. |
type | 첫 번째 INT16으로부터 설정되는 레코드 타입 태그; OR 레코드 디코딩에 필요. |
data | 로우 바이트 — 실제 페이로드; CDC가 소유하고 해제한다. |
OVF_PAGE_LIST — 오버플로(다중 페이지) 로우의 프래그먼트 하나
(log_impl.h):
// struct ovf_page_list -- src/transaction/log_impl.htypedef struct ovf_page_list{ char *rec_type; /* unused on the CDC path (memset to 0) */ char *data; /* fragment incl. OVERFLOW_*_PART header */ int length; /* fragment length */ struct ovf_page_list *next;/* next fragment toward the HEAD (reverse) */} OVF_PAGE_LIST;| 필드 | 역할 / 이유 |
|---|---|
rec_type | 힙 레코드 타입 슬롯; CDC 경로에서는 0으로 초기화되고 사용되지 않는다. |
data | OVERFLOW_*_PART 헤더를 포함한 프래그먼트; 조립 시 오프셋만큼 건너뛰어 제거된다. |
length | 프래그먼트 바이트 길이; recdes->data 크기 산정과 프래그먼트 복사에 쓰인다. |
next | head 방향으로의 연결; prepend 순서이므로 역방향 순회로도 바이트 순서가 맞게 된다. |
LOG_ZIP — 압축 해제 스크래치 (log_compress.h):
// struct log_zip -- src/transaction/log_compress.hstruct log_zip{ LOG_ZIP_SIZE_T data_length = 0; /* decompressed length after log_unzip */ LOG_ZIP_SIZE_T buf_size = 0; /* capacity of log_data */ char *log_data = nullptr; /* the (de)compressed bytes */};| 필드 | 역할 / 이유 |
|---|---|
data_length | log_unzip 이후의 압축 해제 길이 — 실제 이미지 크기. |
buf_size | log_data의 용량; IO_MAX_PAGE_SIZE로 한 번 크기를 잡으며, 초과 시 LZ4 실패. |
log_data | 압축 해제된 바이트 — cdc_get_recdes에서는 스레드 캐시, cdc_get_ovfdata_from_log에서는 호출별 할당. |
불변 조건: RECDES.data가 LOG_ZIP 버퍼를 참조(alias)하는 동안 그 버퍼를
해제해서는 안 된다. 두 fetch 경로 모두 log_zip_free 전에
redo_zip_ptr->log_data에서 malloc된 출력 버퍼로 memcpy를 완료하며, 이를
어기면 다음 레코드가 캐시를 재사용하는 순간 클라이언트에 댕글링 포인터가 전달된다.
7.2 cdc_check_log_page와 cdc_log_read_advance_and_preserve_if_needed
섹션 제목: “7.2 cdc_check_log_page와 cdc_log_read_advance_and_preserve_if_needed”cdc_check_log_page는 버퍼가 해당 LSA의 페이지를 갖고 있음을 보장한다:
log_page_p->hdr.logical_pageid != lsa->pageid이면
logpb_fetch_page (..., LOG_CS_SAFE_READER, ...)를 호출하고, fetch 실패 시
ER_FAILED를, 이미 일치하면 NO_ERROR를 반환하는 no-op이다.
cdc_log_read_advance_and_preserve_if_needed는 헤더가 페이지 경계를 걸칠 때를
처리한다. 페이지 N+1로 진행하기 전에 페이지 N을 보존하는 이유는 호출자가
여전히 N의 헤더 바이트를 필요로 하기 때문이다:
// cdc_log_read_advance_and_preserve_if_needed -- src/transaction/log_manager.cif (lsa->offset + size >= (int) LOGAREA_SIZE) /* <- header would overflow the page */ { memcpy (preserved, log_page_p, IO_MAX_PAGE_SIZE); /* <- keep page N */ lsa->pageid++; if (logpb_fetch_page (thread_p, lsa, LOG_CS_FORCE_USE, log_page_p) != NO_ERROR) { logpb_fatal_error (...); return ER_FAILED; } /* <- fetch failure is FATAL */ lsa->offset = 0; }이것이 cdc_get_recdes가 pageid가 바뀐 뒤 log_page_p를
preserved_log_page_p로 재지정하는 이유다 — 레코드 헤더가 이제 보존된
버퍼 안에 있기 때문이다.
7.3 cdc_get_undo_record — grow-on-fit 방식의 이전 이미지
섹션 제목: “7.3 cdc_get_undo_record — grow-on-fit 방식의 이전 이미지”// cdc_get_undo_record -- src/transaction/log_manager.cundo_recdes->data = (char *) malloc (ONE_K); /* <- optimistic 1 KB */scan_code = log_get_undo_record (thread_p, log_page_p, lsa, undo_recdes);if (scan_code == S_DOESNT_FIT) /* <- 1 KB too small */ { undo_recdes->data = (char *) realloc (undo_recdes->data, (size_t) (-undo_recdes->length)); undo_recdes->area_size = (size_t) (-undo_recdes->length); /* length came back negative */ if (cdc_check_log_page (thread_p, log_page_p, &lsa) != NO_ERROR) return S_ERROR; scan_code = log_get_undo_record (...); /* <- retry exactly once */ }return scan_code; /* S_ERROR / S_END from a non-DOESNT_FIT failure propagate unchanged */처리 분기: (1) 첫 호출에서 S_SUCCESS; (2) S_DOESNT_FIT → 정확히
-length만큼 realloc, 페이지 재검증 후 딱 한 번 재시도, 두 번째 실패는 그대로
전파; (3) 그 밖의 비성공 → 전파. 세 번째 재시도는 없다 —
log_get_undo_record가 정확한 크기를 알려 주므로 한 번의 realloc으로 충분하다.
LOG_ZIP 압축 해제는 log_get_undo_record 내부(vacuum과 공유)에서 일어나므로
여기서는 보이지 않는다.
7.4 cdc_get_recdes — 분기별 간접 추적
섹션 제목: “7.4 cdc_get_recdes — 분기별 간접 추적”cdc_get_recdes는 undo 블록(undo_lsa != NULL일 때)을 먼저 실행한 뒤
redo 블록(redo_lsa != NULL일 때)을 실행하며, 각각 로그 레코드 타입에 대한
switch로 구성된다. is_flashback(정상 CDC 경로에서는 false)은 페이지 소스만
바꾼다: false이면 cdc_Gl.producer.temp_logbuf를 재사용할 수 있고, true이면
항상 재fetch한다. is_flashback == false일 때 페이지 누락이나 체인 손상은
하드 에러다 — 조용히 건너뛰지 않는다.
flowchart TB
A["진입"] --> B{undo_lsa?}
B -->|yes| C["페이지: !is_flashback이면 temp_logbuf,\n아니면 logpb_fetch_page -> 실패 시 error로"]
C --> D["undo 타입 switch: SUPPL/MVCC_UNDOREDO -> cdc_get_undo_record;\nUNDOREDO DEL/UPD -> undo_record 또는 OVF; UNDO/REDO_DATA OVF -> overflow"]
B -->|no| R{redo_lsa?}
D --> R
R -->|no| Z["스크래치 해제, NO_ERROR 반환"]
R -->|yes| F["redo 타입 switch: MVCC_UNDOREDO INSERT/UPDATE_NOTIFY\n-> 이미지 빌드, 필요 시 log_diff; UNDOREDO INSERT 또는 OVF -> 이미지/overflow"]
F --> Z
C -.fetch 실패.-> ERR["error: redo/undo/recdes 해제, error_code 반환"]
Figure 7-2 — cdc_get_recdes. 모든 goto error는 redo_data, undo_data,
tmp_undo_recdes.data, 그리고 두 출력 버퍼를 해제하는 단일 error: 레이블로
집결한다. 분기 상세는 다이어그램 참조.
인용할 만한 redo 케이스는 LOG_MVCC_*UNDOREDO_DATA에서 RVHF_MVCC_INSERT인
경우다: 해당 레코드에는 MVCC 헤더가 없으므로 CDC가 RECDES를 직접 재조립한다
(RVHF_UPDATE_NOTIFY_VACUUM DIFF redo라면 log_diff로 undo 이미지를 패치한다):
// cdc_get_recdes (redo, RVHF_MVCC_INSERT) -- src/transaction/log_manager.credo_recdes->type = *(INT16 *) redo_data;redo_recdes->length = redo_length - sizeof (INT16);tmp_ptr = (char *) redo_data + sizeof (redo_recdes->type);redo_recdes->length += OR_HEADER_SIZE (tmp_ptr); /* <- re-add OR header room */redo_recdes->data = (char *) malloc (redo_recdes->length + MAX_ALIGNMENT);// ... memcpy rep-id, CHN, then body ...두 redo 분기 모두 zip 처리 형태(ZIP_CHECK → log_append_get_zip_redo 스레드
캐시 LOG_ZIP → log_unzip)를 공유하며, 할당이나 압축 해제 실패 시
error:로 점프한다.
7.5 cdc_get_ovfdata_from_log / cdc_get_overflow_recdes — 페이지를 넘나드는 로우
섹션 제목: “7.5 cdc_get_ovfdata_from_log / cdc_get_overflow_recdes — 페이지를 넘나드는 로우”힙 페이지 하나를 초과하는 로우는 prev_tranlsa로 연결된 오버플로 체인으로 저장된다.
cdc_get_overflow_recdes는 체인을 역방향으로 순회하면서 각 프래그먼트를 앞에
prepend하여(결과적으로 순방향 순서가 됨) 페이지별 헤더를 제거한 뒤 연결한다.
head pivot은 시작 위치를 결정한다: 현재 레코드가 방향에 맞는 RVOVF_PAGE_UPDATE
(LOG_UNDO_DATA + is_redo, 또는 LOG_REDO_DATA + !is_redo)이거나
RVOVF_CHANGE_LINK/RVOVF_NEWPAGE_LINK이면, 순회는 prev_tranlsa에서
current_lsa를 시드로 삼아 시작한다(pageid가 바뀌면 페이지를 fetch하고, fetch
실패는 에러). 링크 rcvindex는 데이터가 PAGE_UPDATE 레코드에 있으므로
RVOVF_PAGE_UPDATE로 재작성된다.
순회 루프는 레코드마다 세 가지로 분기한다: (1) 경계 — trid가 바뀌거나
LOG_DUMMY_OVF_RECORD를 만나면 정지하되, undo 경로에서 LOG_DUMMY_OVF_RECORD를
만나면 먼저 prev_lsa를 인수로 cdc_get_ovfdata_from_log를 한 번 더 호출한다
(마지막 프래그먼트가 한 레코드 뒤에 있기 때문); (2) 매칭 — is_redo일 때
redo 레코드(또는 !is_redo일 때 undo 레코드) → fetch 후 prepend; (3) 해당 없으면
페이지 진행. 순회 후 recdes->data는 합산된 length만큼 할당되고 head부터
채워진다; 첫 번째 노드는 area_offset = offsetof (OVERFLOW_FIRST_PART, data),
나머지는 offsetof (OVERFLOW_REST_PART, data)로 오프셋을 잡아 페이지별 헤더를
제거한다.
불변 조건: 모든 OVF_PAGE_LIST 노드와 그 data는 정확히 한 번만 해제된다.
성공 경로는 각 노드를 드레인하면서 해제하고, end: 레이블은 에러 시 나머지를
드레인하며, 두 goto end 지점은 점프 전에 현재 노드를 해제하므로 이중 해제가
발생하지 않는다. 이 조건이 깨지면 체인 중간의 malloc 실패가 누수를 일으킨다.
cdc_get_ovfdata_from_log 자체는: 헤더를 건너뛰고, is_redo에 따라
LOG_REC_REDO/LOG_REC_UNDO를 읽은 뒤, rcvindex가 일치하지 않으면 데이터
없이 goto end한다(에러가 아닌 skip). 일치하면 length만큼 복사하고,
ZIP_CHECK/log_unzip을 호출별로 log_zip_alloc한 LOG_ZIP으로 처리한
뒤(end:에서 해제; cdc_get_recdes의 스레드 캐시 zip과 별개), *outdata에
새로 할당한 버퍼로 memcpy한다.
7.6 cdc_make_dml_loginfo — 두 RECDES를 하나의 바이트 블롭으로
섹션 제목: “7.6 cdc_make_dml_loginfo — 두 RECDES를 하나의 바이트 블롭으로”undo_recdes(이전)와 redo_recdes(이후)를 갖고 패킹하면 CDC_LOGINFO_ENTRY가
만들어진다:
// struct cdc_loginfo_entry -- src/transaction/log_impl.htypedef struct cdc_loginfo_entry{ LOG_LSA next_lsa; /* set by the dispatch caller, not here */ int length; /* byte length of log_info */ char *log_info; /* the packed wire payload */} CDC_LOGINFO_ENTRY;flowchart TB
A["진입"] --> P{root_class_oid와 heap_attrinfo_start 성공?}
P -->|no| PE["flashback이면 스키마 변경 오류\n아니면 cdc_make_error_loginfo; goto exit"]
P -->|yes| U["undo+redo: 스키마 확인, dbvalue 읽기\n-> def_order 순으로 old_values / new_values"]
U --> PK{all_in_cond==0 이고 INSERT 아니고 flashback 아님?}
PK -->|yes| FPK["cdc_find_primary_key -> has_pk; has_pk<0 -> goto exit"]
PK -->|no| SW
FPK --> SW["dml_type switch: INSERT 전체 변경;\nUPDATE 변경=diff cond=PK/전체; DELETE 변경 없음 cond=PK/전체"]
SW --> FIN["length 계산, log_info malloc, memcpy;\nexit: 해제, ER_CDC_LOGINFO_ENTRY_GENERATED 반환"]
Figure 7-3 — cdc_make_dml_loginfo. 모든 조기 실패는 loginfo_buf, 인덱스/값
배열, attrinfo를 해제하는 단일 exit: 레이블로 라우팅된다.
설정. 파티션 클래스 OID는 partition_find_root_class_oid로 루트 OID로
재작성된다; 실패(클래스 삭제)하면 함수는 에러 loginfo(§7.9)를 발행하고 종료한다
— flashback에서는 FLASHBACK_ERROR_HANDLING이 먼저 ER_FLASHBACK_SCHEMA_CHANGED를
발생시킨다. heap_attrinfo_start는 최신 클래스 표현을 로드한다.
def_order로 값 읽기. NULL이 아닌 각 RECDES마다 CDC는
cdc_check_if_schema_changed(§7.10)를 호출한 뒤 heap_attrinfo_read_dbvalues를
호출하고, 각 값을
old_values[heap_value->read_attrepr->def_order](저장 순서가 아닌 정의 순서)에
복사하면서 cdc_get_attribute_size(§7.8)를 record_length에 누적한다;
new_values도 redo_recdes로부터 동일하게 채워진다. 기본 키 조회는
all_in_cond == 0이고, INSERT가 아니며, flashback도 아닐 때만 실행된다;
has_pk < 0(함수 인덱스 PK)은 하드 goto exit이다.
7.7 변경 컬럼과 조건 컬럼의 분리
섹션 제목: “7.7 변경 컬럼과 조건 컬럼의 분리”DML 페이로드의 핵심으로, 클라이언트 DML 구조체(Chapter 8)와 대응된다. 각 분기는
순서대로 dml_type, classoid, num_change_col + 변경 인덱스 + 값, 그리고
num_cond_col + 조건 인덱스 + 값을 패킹한다.
- INSERT / TRIGGER_INSERT: 모든 컬럼이 변경됨
(
num_change_col = attr_info.num_values,new_values에서 가져옴); 조건 컬럼 없음. - UPDATE / TRIGGER_UPDATE: 변경 집합 = 컬럼별 diff(아래); 조건 컬럼 = PK
(
has_pk == 1이면pk_attr_index에서), PK가 없으면old_values전체. Flashback에서는 변경 집합이 모든 컬럼으로 강제된다. - DELETE / TRIGGER_DELETE:
num_change_col = 0; 조건 컬럼 = PK(또는 전체)에서old_values.
UPDATE 변경 집합은 cdc_compare_undoredo_dbvalue로 계산된다:
// cdc_make_dml_loginfo (UPDATE changed set) -- src/transaction/log_manager.cfor (i = 0; i < attr_info.num_values; i++) if (cdc_compare_undoredo_dbvalue (&new_values[i], &old_values[i]) > 0) /* <- only differing cols */ changed_col_idx[cnt++] = i;if (cnt == 0) { error_code = ER_CDC_IGNORE_LOG_INFO_INTERNAL; goto exit; } /* <- trigger-savepoint phantom */cdc_compare_undoredo_dbvalue 분기별 분석. 두 피연산자가 NULL이 아닌
포인터임을 assert한 뒤, 실질적인 분기는 정확히 두 가지다:
// cdc_compare_undoredo_dbvalue -- src/transaction/log_manager.cassert (new_value != NULL && old_value != NULL);if (DB_IS_NULL (new_value) && DB_IS_NULL (old_value)) return 0; /* <- both SQL NULL == SAME, not a change */else return db_value_compare (new_value, old_value) == 0 ? 0 : 1; /* <- 1 == different */both-NULL 분기는 필수적이다: db_value_compare는 두 NULL을 동등하게 처리하지
않으므로, 명시적 return 0 없이는 이전과 이후 모두 NULL인 컬럼이 잘못 변경된
것으로 처리된다. 이 분기가 그 컬럼을 diff에서 제외하므로, > 0은 실제로 달라진
컬럼만 선택한다. 빈 diff(cnt == 0)는 trigger-savepoint 팬텀이다 —
에러가 아니라 ER_CDC_IGNORE_LOG_INFO_INTERNAL로 무시된다.
불변 조건: 조건 컬럼은 항상 old_values를, 변경 컬럼은 항상 new_values를
사용한다. 조건 컬럼은 클라이언트의 WHERE 키이고, 변경 컬럼은 SET 페이로드다;
이를 교차하면 클라이언트가 post-image를 기준으로 변경을 적용하려다 행을 찾지 못하게
된다.
역패치 및 복사. dml_entry->length = ptr - start_ptr 후
or_pack_int (start_ptr, dml_entry->length)가 더미 길이 접두사를 제자리에서
덮어쓴다; 블롭은 새로 malloc한 dml_entry->log_info에 memcpy된다. 함수는
ER_CDC_LOGINFO_ENTRY_GENERATED(성공 센티널)를 반환하며, exit:는 스크래치,
인덱스/값 배열, attrinfo를 해제한다.
7.8 cdc_get_attribute_size와 cdc_put_value_to_loginfo
섹션 제목: “7.8 cdc_get_attribute_size와 cdc_put_value_to_loginfo”cdc_get_attribute_size는 DB_VALUE의 직렬화 바이트 예산을 반환한다(바이트를
직접 출력하지는 않음). DB_VALUE_TYPE에 따라 switch: 고정 크기 타입 → C 폭;
NUMERIC → DB_NUMERIC_BUF_SIZE; BIT/VARBIT → X'..' 텍스트 길이;
CHAR/VARCHAR → db_get_string_size; DATE/TIME → *_PRECISION 상수(문자열로
직렬화); MONETARY → double + 통화 기호; ENUMERATION → 문자열 형태 또는 short;
BLOB/CLOB → ELO locator 길이; 지원하지 않는 복합 타입(OBJECT, SET, JSON)
→ 0; default는 assert.
cdc_put_value_to_loginfo는 func_type 태그를 앞에 붙인 뒤 값을 출력한다 —
클라이언트의 디코드 판별자(Chapter 8):
func_type | 대상 | 패킹 방식 |
|---|---|---|
| 0 | INTEGER | or_pack_int |
| 1 | BIGINT | or_pack_int64 |
| 2 | FLOAT | or_pack_float |
| 3 | DOUBLE | or_pack_double |
| 4 | SHORT, 그리고 short로 저장되는 ENUMERATION(db_get_enum_string == NULL) | or_pack_short |
| 7 | NULL, NUMERIC, BIT/VARBIT, CHAR/VARCHAR, 모든 DATE/TIME, MONETARY, 문자열 enum, BLOB/CLOB locator, 지원 안 되는 복합 타입 전체 | or_pack_int(7) + or_pack_string(text 또는 NULL) |
func_type 5와 6은 예약됨(출력되지 않음); DATE/TIME 타입은 db_to_char 포맷
문자열만 다르다(예: DB_TYPE_DATE → "YYYY-MM-DD").
// cdc_put_value_to_loginfo (NULL guard) -- src/transaction/log_manager.cif (DB_IS_NULL (new_value)) { func_type = 7; ptr = or_pack_int (ptr, func_type); ptr = or_pack_string (ptr, NULL); /* <- NULL packs as func_type 7 + NULL string */ *data_ptr = ptr; return NO_ERROR; /* <- NULL is normal (e.g. ALTER added a column) */ }NULL 값은 에러가 아니다 — ALTER로 추가된 컬럼이 나타나는 방식이다. default는
assert하며, LOB 추출 및 ELO_LO 같은 하드 실패는 ER_FAILED를 반환하고, 호출자가
이를 goto exit으로 변환한다.
7.9 cdc_make_error_loginfo — 퇴화된 DML 페이로드
섹션 제목: “7.9 cdc_make_error_loginfo — 퇴화된 DML 페이로드”스키마를 해소할 수 없을 때(클래스 삭제, 스키마 변경), cdc_make_dml_loginfo는
에러 DML 엔트리를 발행한다: 동일한 CDC_DML 프레이밍과 실제
trid/user/dml_type/classoid를 유지하되,
num_change_col = 0이고 num_cond_col = 0이다. 클라이언트는 컬럼 데이터가 없는
well-formed DML 이벤트를 받게 되므로, 잘린 레코드로 질식하는 대신 skip/log/abort
등의 대응을 선택할 수 있다. 길이를 역패치하고 dml_entry->log_info에 복사한 뒤
ER_CDC_LOGINFO_ENTRY_GENERATED를 반환한다.
7.10 cdc_find_primary_key와 cdc_check_if_schema_changed
섹션 제목: “7.10 cdc_find_primary_key와 cdc_check_if_schema_changed”cdc_find_primary_key는 repr_id를 PK 컬럼의 def_order 배열로 매핑한다:
heap_classrepr_get으로 클래스 rep을 로드하고, 스캔으로 BTREE_PRIMARY_KEY를
찾아 히트 시 pk_attr[]를 채운다:
// cdc_find_primary_key -- src/transaction/log_manager.cif (index->type == BTREE_PRIMARY_KEY) { has_pk = 1; if (index->func_index_info != NULL) return ER_FAILED; /* <- func-index PK unsupported */ num_idx_att = index->n_atts; pk_attr = (int *) malloc (sizeof (int) * num_idx_att); for (int j = 0; j < num_idx_att; j++) { pk_attr[j] = index->atts[j]->def_order; *num_attr += 1; } /* <- PK col def_orders */ *pk_attr_id = pk_attr; break; }반환값: 1(PK 발견 — 호출자가 def_order 배열을 조건 컬럼으로 사용), 0(PK 없음
— 호출자가 모든 컬럼으로 폴백, §7.7), 또는 함수 인덱스 PK(ER_FAILED) / 할당
실패(ER_OUT_OF_VIRTUAL_MEMORY)일 때 음수. 소스 내 헤더 주석은 이중으로 낡아
있다: “PK가 있으면 PK 컬럼 id와 함께 0 반환”(실제 코드는 1 반환), “PK가 없으면
-1 반환”(실제 코드는 has_pk 즉 0 반환 — 음수는 함수 인덱스와 OOM 경로에
예약됨). 반환된 def_order 인덱스는 값 배열을 직접 인덱싱한다.
cdc_check_if_schema_changed는 한 줄이지만 중요도가 높다:
// cdc_check_if_schema_changed -- src/transaction/log_manager.creturn or_rep_id (recdes) != attr_info->last_classrepr->id;레코드의 표현 id를 최신 id와 비교한다. CDC는 스키마가 변경된 이후에 작성된
레코드를 해석할 수 없다 — 옛 rep에는 CDC가 필요로 하는 def_order 메타데이터가
없고, 클라이언트는 JDBC로 스키마를 미리 fetch하기 때문이다. 따라서 불일치 시
최선 노력(best-effort) 디코딩 대신 cdc_make_error_loginfo(또는 flashback에서는
ER_FLASHBACK_SCHEMA_CHANGED)로 라우팅된다.
7.11 cdc_make_ddl_loginfo — 인라인 구문 텍스트
섹션 제목: “7.11 cdc_make_ddl_loginfo — 인라인 구문 텍스트”DDL은 훨씬 단순하다: 보조 레코드에 이미 완전한 구문 텍스트가 담겨 있으므로 로우 구체화가 필요 없다 — CDC는 supplement를 언팩하고 DDL 와이어 형태로 재팩한다:
// cdc_make_ddl_loginfo -- src/transaction/log_manager.cptr = or_unpack_int (ptr, &ddl_type);ptr = or_unpack_int (ptr, &object_type);ptr = or_unpack_oid (ptr, &classoid);if (!OID_ISNULL (&classoid)) if (oid_is_system_class (&classoid) || !cdc_is_filtered_class (classoid)) { error_code = ER_CDC_IGNORE_LOG_INFO; goto error; } /* <- system/unfiltered class: skip */ptr = or_unpack_oid (ptr, &oid); /* then statement_length + statement */시스템 클래스이거나 CDC 필터 밖의 클래스이면 ER_CDC_IGNORE_LOG_INFO — 실패가
아닌 skip이다. 그렇지 않으면 출력에 length / trid / user / CDC_DDL /
ddl_type / object_type / objectOID / classOID / statement_length / statement를
패킹하고, 길이를 역패치한 뒤 ddl_entry->log_info에 복사하고
ER_CDC_LOGINFO_ENTRY_GENERATED를 반환한다; error: 레이블은 loginfo_buf를
해제한다.
7.12 챕터 요약 — 핵심 정리
섹션 제목: “7.12 챕터 요약 — 핵심 정리”cdc_get_recdes는 LSA를RECDES이미지로 해소한다 — undo 블록 후 redo 블록, 각각 타입 switch로cdc_get_undo_record를 통한 fetch, MVCC 삽입 이미지 직접 재조립, 또는 DIFF 레코드에 대한log_diff적용을 수행한다.- 페이지 경계는 명시적으로 처리된다:
cdc_log_read_advance_and_preserve_if_needed가 경계를 넘기 전에 페이지를 보존하고,cdc_get_recdes는 pageid가 바뀌었을 때log_page_p를 보존된 버퍼로 재지정한다. - 오버플로 로우는 역방향으로 재조립한 뒤 순방향으로 연결된다.
cdc_get_overflow_recdes는prev_tranlsa를 순회하면서 각OVF_PAGE_LIST프래그먼트를 prepend하고,OVERFLOW_*_PART헤더를 제거한 뒤 각 노드를 한 번만 해제한다;cdc_get_ovfdata_from_log는 호출별LOG_ZIP압축 해제를 담당한다. cdc_make_dml_loginfo는 값을def_order로 재정렬하고 컬럼을 변경 집합 (new_values)과 조건 집합(old_values)으로 분리한다 — PK가 있으면 PK, 없으면 전체 — SET/WHERE 분리다.cdc_compare_undoredo_dbvalue는 두 SQL NULL을 동일하게 처리(return 0)하여 diff가 이전·이후 모두 NULL인 컬럼을 건너뛰게 한다; 빈 diff는ER_CDC_IGNORE_LOG_INFO_INTERNAL로 무시된다. 직렬화는func_type0–4와 NULL/ 문자열 형태를 위한 catch-all 7을 사용한다(5–6은 미사용).- 스키마 이탈은 스트림이 아닌 해석에 치명적이다:
cdc_check_if_schema_changed불일치(또는 클래스 삭제)는cdc_make_error_loginfo로 라우팅되어 컬럼은 없지만 well-formed인CDC_DML엔트리를 생성한다. - DDL은 구체화를 건너뛴다 —
cdc_make_ddl_loginfo는 인라인 구문 텍스트를 재팩하며, 시스템/비필터 클래스는 에러가 아닌ER_CDC_IGNORE_LOG_INFO로 skip한다.
Chapter 8: 클라이언트 API 생애주기와 와이어 디코딩
섹션 제목: “Chapter 8: 클라이언트 API 생애주기와 와이어 디코딩”이 챕터는 서버 측 분석(Ch. 5–7)에서 시선을 돌려 외부 소비자(external consumer) —
src/api/cubrid_log.c에 정의된 cubrid_log_* C ABI — 를 다룬다. 하나의 세션이
어떤 단계를 거쳐 네트워크에서 배치(batch)를 가져오고, 바이트 덩어리를 타입 있는
CUBRID_LOG_ITEM으로 디코딩하는지를 살펴본다. 풀(pull) 방식 설계의 전반적인
맥락은 cubrid-cdc.md §“Public API surface”를 참조한다. 여기서 설명하는 모든
것은 단일 스레드이며 전역 상태로 구동된다: 암묵적인 세션 하나가 파일 범위
g_* 변수들에 담겨 있고, 핸들(handle)이 없다. 이 구조가 프로세스당 세션 하나라는
제약을 낳으며, 스테이지 머신(stage machine)이 왜 중요한지를 설명한다.
8.1 두 열거형과 아이템 구조체
섹션 제목: “8.1 두 열거형과 아이템 구조체”// CUBRID_LOG_STAGE -- src/api/cubrid_log.ctypedef enum { CUBRID_LOG_STAGE_CONFIGURATION, CUBRID_LOG_STAGE_PREPARATION, CUBRID_LOG_STAGE_EXTRACTION, CUBRID_LOG_STAGE_FINALIZATION } CUBRID_LOG_STAGE;CUBRID_LOG_STAGE g_stage = CUBRID_LOG_STAGE_CONFIGURATION; /* the whole session state */| 값 | 역할 / 존재 이유 |
|---|---|
CONFIGURATION | 초기 상태 + finalize 이후 상태. 7개의 setter는 오직 이 단계에서만 합법이므로, START_SESSION에서 서버가 읽기 전에 필터와 타임아웃이 고정된다. |
PREPARATION | connect_server 이후. find_lsa와 첫 extract가 합법. “연결됨, 커서 없음” 상태와 “추출 중” 상태를 구분한다. |
EXTRACTION | 첫 extract 이후. extract와 clear_log_item이 합법. 다음 풀(pull) 전에 반드시 비워야 할 라이브 배치 버퍼(live batch buffer)가 존재함을 표시한다. |
FINALIZATION | 선언은 되어 있지만 실제로는 할당되지 않음. finalize는 CONFIGURATION으로 돌아간다. 흔적만 남은 값 — CONFIGURATION이 종료 상태를 겸한다. |
불변 조건: 공개 진입점(public entry point)은 다른 전역 변수에 접근하기 전에
반드시 g_stage를 먼저 확인한다. 첫 번째 문장은 g_stage != <required> 검사로,
조건 불일치 시 INVALID_FUNC_CALL_STAGE (-27)를 반환한다. 이 검사가 없으면
소비자가 스트림 도중에 set_max_log_item을 재실행해 협상된 세션을 오염시키거나,
연결 전에 extract를 호출해 null인 g_conn_entry를 역참조할 수 있다. 이후 절에서는
이 규칙에서 벗어나는 경우에만 별도로 언급한다.
// DATA_ITEM_TYPE -- src/api/cubrid_log.ctypedef enum { DATA_ITEM_TYPE_DDL = 0, DATA_ITEM_TYPE_DML, DATA_ITEM_TYPE_DCL, DATA_ITEM_TYPE_TIMER } DATA_ITEM_TYPE;| 값 | 역할 / 존재 이유 |
|---|---|
DDL (0) | 스키마 변경. ddl 유니온 팔(arm)을 선택. SQL 텍스트가 인라인으로 포함되어 리플레이(replay) 시 카탈로그 조회가 불필요하다. |
DML (1) | 행 변경. dml 팔을 선택. malloc/free가 필요한 컬럼 배열을 보유하는 유일한 팔이다. |
DCL (2) | 트랜잭션 제어. dcl 팔을 선택. 커밋 타임스탬프로 트랜잭션 내 행들의 앞뒤를 묶는다. |
TIMER (3) | 타임스탬프 하트비트. timer 팔을 선택. 유휴 구간에서 커서를 전진시킨다. |
이 값들은 와이어(wire) 상에서도 의미를 가진다: 서버가 raw int로 패킹하고,
make_log_item이 이를 DATA_ITEM_TYPE으로 캐스트해 분기하며,
data_item_type_to_string은 0..3 범위를 벗어나면 assert(0)한다.
// CUBRID_LOG_ITEM -- src/api/cubrid_log.hstruct cubrid_log_item { int transaction_id; char *user; int data_item_type; CUBRID_DATA_ITEM data_item; CUBRID_LOG_ITEM *next; };| 필드 | 역할 / 존재 이유 |
|---|---|
transaction_id | 소스 trid 기준으로 아이템을 묶는다. 커밋 경계는 DCL 아이템으로 전달된다. |
user | 클라이언트 사용자 이름. or_unpack_string_nocopy로 g_log_infos를 가리키는 포인터 — 소유권 없음, blob이 재사용되면 무효화된다. |
data_item_type | 활성 유니온 팔을 선택한다. 와이어 포맷과 맞추기 위해 raw int; 분기 시 캐스트한다. |
data_item | 타입 있는 페이로드 (CUBRID_DATA_ITEM 유니온). 구조체 하나가 네 가지를 모두 커버하며, 힙 메모리를 사용하는 건 DML뿐이다. |
next | NULL 종단 리스트 링크. list_size(별도 반환)를 쓰지 않고 순회 가능. |
flowchart LR
subgraph G["파일 범위 전역 변수"]
BLOB["g_log_infos\n(바이트 blob, 재사용됨)"]
ITEMS["g_log_items[]\n(CUBRID_LOG_ITEM 배열, 재사용됨)"]
end
ITEMS -->|next| ITEMS
ITEMS -->|"user, statement,\ncolumn_data 포인터"| BLOB
ITEMS -->|"data_item 유니온"| U["DDL | DML | DCL | TIMER"]
Figure 8-1 — g_log_items[]는 풀(pull)이 일어날 때마다 재사용된다. 문자열 및
컬럼 포인터는 g_log_infos를 별칭(alias)한다. DML 팔의 index/data/len 배열만
별도로 malloc된다.
8.2 CONFIGURATION 단계 — 7개의 setter
섹션 제목: “8.2 CONFIGURATION 단계 — 7개의 setter”7개 모두 동일한 골격을 공유한다: g_stage == CONFIGURATION 검사, 범위 검사,
g_* 전역에 저장, SUCCESS 반환. 차이는 범위 검사와 오류 코드뿐이다.
| Setter | 전역 | 범위 / 규칙 | 오류 |
|---|---|---|---|
set_connection_timeout | g_connection_timeout | 0..360 | INVALID_CONNECTION_TIMEOUT |
set_extraction_timeout | g_extraction_timeout | 0..360 | INVALID_EXTRACTION_TIMEOUT |
set_max_log_item | g_max_log_item | 1..1024 | INVALID_MAX_LOG_ITEM |
set_all_in_cond | g_all_in_cond | 0..1 | INVALID_RETRIEVE_ALL |
set_extraction_table | g_extraction_table[] | 배열과 크기 모두 null이거나 모두 설정 | INVALID_CLASSOID_ARR_SIZE |
set_extraction_user | g_extraction_user[] | 배열과 크기 모두 null이거나 모두 설정 | INVALID_USER_ARR_SIZE |
set_tracelog | g_trace_log_* | 경로 쓰기 가능, level 0..1, filesize 8..512 MB | INVALID_PATH/LEVEL/FILESIZE |
// cubrid_log_set_extraction_table -- src/api/cubrid_log.c if ((classoid_arr == NULL && arr_size != 0) || (classoid_arr != NULL && arr_size == 0)) return CUBRID_LOG_INVALID_CLASSOID_ARR_SIZE; /* <- XOR null/size mismatch */ g_extraction_table = (uint64_t *) malloc (sizeof (uint64_t) * arr_size); if (g_extraction_table == NULL) return CUBRID_LOG_FAILED_MALLOC; memcpy (g_extraction_table, classoid_arr, arr_size * sizeof (uint64_t)); /* OIDs are POD */set_extraction_user는 각 문자열을 strdup하지만 strdup 반환값을 검사하지
않는다 — OOM 발생 시 NULL 슬롯이 남고, 이후 or_pack_string에서 크래시가
발생한다. set_tracelog는 check_tracelog_path에서 mkdir하고
(→ NO_FILE_PERMISSION), filesize == -1을 10 MB 기본값으로 처리한다.
불변 조건: setter는 절대 네트워크에 접근하지 않는다. 서버는 나중에
send_configurations에서 설정을 한 번만 읽는다. 스테이지 가드가 연결 이후 setter
재실행을 차단하므로, 클라이언트 측에서 스트림 도중 필터 경쟁 조건이 발생하지
않는다 (cubrid-cdc.md Open Question 4).
8.3 PREPARATION 단계 — 연결
섹션 제목: “8.3 PREPARATION 단계 — 연결”cubrid_log_connect_server는 CONFIGURATION → PREPARATION의 관문으로, 세 개의
헬퍼를 순서대로 실행하고 모두 성공했을 때만 g_stage를 전진시킨다. g_dbname은
(CUBRID_LOG_MAX_DBNAME_LEN으로 상한이 걸린) strncpy가 스테이지 가드 이전에
수행되므로, 스테이지 오용(misuse) 호출이라도 g_dbname은 변경됨을 유의한다.
flowchart TB
A["connect_server"] --> B{"dbname != NULL?"}
B -->|no| ERR["INVALID_DBNAME"]
B -->|yes| C["g_dbname에 strncpy"]
C --> D{"stage == CONFIGURATION?"}
D -->|no| ERR2["INVALID_FUNC_CALL_STAGE"]
D -->|yes| E{"host/port/user/password 유효?"}
E -->|no| ERR3["INVALID_HOST/PORT/USER/PASSWORD"]
E -->|yes| F["db_login"]
F -->|실패| ERR4["FAILED_LOGIN"]
F -->|성공| G["er_init"] --> H["connect_server_internal"]
H -->|실패| ERR5["FAILED_CONNECT"]
H -->|성공| I["send_configurations"]
I -->|실패| ERR6["FAILED_CONNECT / UNAVAILABLE_CDC_SERVER"]
I -->|성공| J["g_stage = PREPARATION; SUCCESS"]
Figure 8-2 — cubrid_log_connect_server 분기 맵.
cubrid_log_db_login은 자격증명 검증만을 위해 db_restart를 전체 실행하며,
DBA 멤버십을 확인하고 모든 종료 경로에서 db_shutdown을 호출한다:
// cubrid_log_db_login -- src/api/cubrid_log.c if (au_login (username, password, true) != NO_ERROR) goto error; if (db_restart ("cubrid_log_api", 0, dbname_at_hostname) != NO_ERROR) return CUBRID_LOG_FAILED_LOGIN; /* <- early return, NOT goto error */ if ((user = au_find_user (username)) == NULL) goto error; if (!au_is_dba_group_member (user)) goto error; /* <- CDC requires DBA */ db_shutdown (); return CUBRID_LOG_SUCCESS;error: db_shutdown (); return CUBRID_LOG_FAILED_LOGIN; /* every failure -> -33 */db_restart 실패는 직접 반환한다(해제할 것이 없으므로 goto error가 아님).
db_restart 이후 세 가지 실패 경로는 먼저 db_shutdown을 수행하기 위해
goto error로 분기한다.
cubrid_log_connect_server_internal은 CSS 연결을 맺고 1-int “reason” 값을
수신한다:
css_make_conn→css_connect_to_log_server→css_receive_data(타임아웃g_connection_timeout초); 실패 시 →FAILED_CONNECT. 응답은 정확히sizeof(int)이어야 하며,reason = ntohl(*recv_data).- non-WINDOWS:
reason == SERVER_CONNECTED요구, 아니면FAILED_CONNECT. - WINDOWS:
reason == SERVER_CONNECTED_NEW요구 후 2단계 핸드셰이크 — 새 포트 id 수신,css_close_conn,css_server_connect_part_two(마스터→서버 포트 핸드오프).
cubrid_log_error 레이블은 recv_data를 해제하고,
css_queue_find_and_remove_header_entry_ptr(g_conn_entry, rid),
css_free_conn, (Windows) css_windows_shutdown을 실행한다 — 네트워크 헬퍼
전반에 반복되는 find-and-remove-then-free 패턴이다.
cubrid_log_send_configurations는 미리 계산된 크기의 버퍼로 세션을 열고 응답
코드에 따라 분기한다:
// cubrid_log_send_configurations -- src/api/cubrid_log.c request_size = OR_INT_SIZE * 5; /* 5 fixed ints + 2 variable arrays */ for (...) request_size += or_packed_string_length (g_extraction_user[i], NULL); request_size += (OR_BIGINT_SIZE * g_extraction_table_count); ...or_pack_int (max_log_item, extraction_timeout, all_in_cond, user_count)... for(...) or_pack_string (g_extraction_user[i]); ...or_pack_int (table_count); for(...) or_pack_int64 (g_extraction_table[i]); ...NET_SERVER_CDC_START_SESSION... or_unpack_int (recv_data, &reply_code); if (reply_code == ER_CDC_NOT_AVAILABLE) CUBRID_LOG_ERROR_HANDLING (CUBRID_LOG_UNAVAILABLE_CDC_SERVER, "...supplemental_log..."); else if (reply_code != NO_ERROR) CUBRID_LOG_ERROR_HANDLING (CUBRID_LOG_FAILED_CONNECT, ...);ER_CDC_NOT_AVAILABLE → UNAVAILABLE_CDC_SERVER (-34), supplemental_log
힌트를 포함(서버가 CDC 설정 없이 기동됨). 그 외 non-NO_ERROR →
FAILED_CONNECT.
8.4 PREPARATION 단계 — 시작 LSA 시딩
섹션 제목: “8.4 PREPARATION 단계 — 시작 LSA 시딩”cubrid_log_find_lsa (전체 시딩 의미론은 Ch. 4 참조)는 스테이지/타임스탬프/
출력 파라미터를 검사한 뒤, 커서가 아직 없는 경우에만 서버에 질의한다:
// cubrid_log_find_lsa -- src/api/cubrid_log.c ...guards: g_stage != PREPARATION; timestamp NULL or < 0; lsa NULL... if (LSA_ISNULL (&g_next_lsa)) /* <- query server only if no cursor yet */ if (cubrid_log_find_start_lsa (timestamp, &g_next_lsa) != SUCCESS) { ...LSA_NOT_FOUND... } memcpy (lsa, &g_next_lsa, sizeof (uint64_t)); /* <- return the cursor */불변 조건: 커서가 존재하면 find_lsa는 멱등(idempotent) — 이후 호출은
g_next_lsa를 그대로 반환하므로, 소비자는 추출을 시작한 뒤 역방향으로
다시 시킹(re-seek)할 수 없다.
cubrid_log_find_start_lsa는 타임스탬프를 int64로 패킹하고
NET_SERVER_CDC_FIND_LSA를 전송한 뒤 reply_code | lsa | timestamp를 언패킹한다.
잠재 버그: 첫 번째 if가 NO_ERROR || ER_CDC_ADJUSTED_LSA를 모두 매칭하고
평범한 SUCCESS를 반환하므로, 뒤이어 오는
else if (... ER_CDC_ADJUSTED_LSA) — SUCCESS_WITH_ADJUSTED_LSA (2)를
반환해야 하는 분기 — 가 **죽은 코드(dead code)**가 된다. 그 외 코드는
LSA_NOT_FOUND.
8.5 EXTRACTION 단계 — 2-요청 풀
섹션 제목: “8.5 EXTRACTION 단계 — 2-요청 풀”cubrid_log_extract는 루프 바디로, PREPARATION과 EXTRACTION 모두에서
합법이며(첫 호출은 PREPARATION에서 발생) 항상 EXTRACTION으로 끝난다.
소비자의 *lsa가 진입 시 g_next_lsa를 덮어쓰므로 애플리케이션이 커서를
소유한다(다음 lsa를 영속화하고, 재연결 시 재실행 — cubrid-cdc.md §“A modern
CDC pull, end to end”). 반환 시 *lsa는 다음 커서를 담는다.
// cubrid_log_extract -- src/api/cubrid_log.c ...guards: stage not PREPARATION/EXTRACTION; lsa/log_item_list/list_size NULL... memcpy (&g_next_lsa, lsa, sizeof (LOG_LSA)); /* <- caller's cursor wins */ rc = cubrid_log_extract_internal (&g_next_lsa, &num_infos, &total_length); ... rc = cubrid_log_make_log_item_list (num_infos, total_length, log_item_list, list_size); ... memcpy (lsa, &g_next_lsa, sizeof (uint64_t)); /* <- hand advanced cursor back */ g_stage = CUBRID_LOG_STAGE_EXTRACTION;flowchart TB
A["g_next_lsa 패킹"] --> B["GET_LOGINFO_METADATA 전송"]
B --> C["reply_code | next_lsa | num_infos | total_length 수신"]
C --> D{"reply_code?"}
D -->|ER_CDC_EXTRACTION_TIMEOUT| E["EXTRACTION_TIMEOUT 반환"]
D -->|ER_CDC_INVALID_LOG_LSA| F["INVALID_LSA 반환"]
D -->|NO_ERROR / other| G["next_lsa, num_infos, total_length 언패킹"]
G --> H{"g_log_infos_size < total_length?"}
H -->|yes| I["total_length + MAX_ALIGNMENT로 realloc"]
H -->|no| J["blob 재사용"]
I --> K{"total_length > 0?"}
J --> K
K -->|yes| L["GET_LOGINFO 전송\nblob을 g_log_infos에 수신"]
K -->|no| M["2번째 요청 생략"]
L --> N["SUCCESS 반환"]
M --> N
Figure 8-3 — cubrid_log_extract_internal. 다이어그램이 표현하지 못하는 두
가지 분기 문제: (a) reply_code 분기에 else 팔이 없다 — 두 ER_CDC_*
케이스를 통과한 non-NO_ERROR 코드는 성공 응답으로 언패킹된다(취약한 동작);
(b) rc == SUCCESS_WITH_NO_LOGITEM 검사는 도달 불가(unreachable) goto를
가드하는데, 그 시점에 rc는 여전히 NO_ERROR이므로 그 경로는 죽은
코드(dead code) — 빈 배치는 num_infos == 0으로 신호된다.
불변 조건: 메타데이터 왕복(round-trip)이 버퍼 크기를 결정하고 나서 데이터
왕복이 채운다 — 클라이언트는 g_log_infos를 단방향 grow-only realloc으로
키우고(실패 시 → FAILED_MALLOC), 두 번째 요청은 total_length > 0일 때만
실행된다. 수신 크기가 total_length와 다르면 FAILED_CONNECT; 버퍼가 작으면
힙 오버런이 발생한다.
8.6 디코딩 체인 — blob에서 타입 아이템으로
섹션 제목: “8.6 디코딩 체인 — blob에서 타입 아이템으로”// cubrid_log_make_log_item_list -- src/api/cubrid_log.c if (g_log_items_count < num_infos) { ...realloc g_log_items, never shrink... } ptr = PTR_ALIGN (g_log_infos, MAX_ALIGNMENT); for (i = 0; i < num_infos; i++) { cubrid_log_make_log_item (&ptr, &g_log_items[i]); g_log_items[i].next = &g_log_items[i + 1]; /* <- link forward */ ptr = PTR_ALIGN (ptr, MAX_ALIGNMENT); } /* <- realign between records */ g_log_items[num_infos - 1].next = NULL; /* <- terminate (underflows if 0) */불변 조건: 각 레코드는 다음 레코드를 디코딩하기 전에 MAX_ALIGNMENT로
재정렬된다 — 서버가 경계 단위로 패킹하므로, PTR_ALIGN을 생략하면
다음 or_unpack_*이 잘못된 데이터를 읽는다.
// cubrid_log_make_log_item -- src/api/cubrid_log.c (per-item header, then dispatch) ptr = or_unpack_int (ptr, &log_info_len); /* <- length prefix, skipped */ ptr = or_unpack_int (ptr, &log_item->transaction_id); ptr = or_unpack_string_nocopy (ptr, &log_item->user); /* <- aliases into blob */ ptr = or_unpack_int (ptr, &log_item->data_item_type); rc = cubrid_log_make_data_item (&ptr, (DATA_ITEM_TYPE) log_item->data_item_type, &log_item->data_item);cubrid_log_make_data_item은 4방향 switch다 (default: assert(0)):
make_ddl은 ddl_type/object_type/oid/classoid/statement_length를 언패킹한
뒤 or_unpack_string_nocopy로 statement를 얻는다(SQL은 blob의 별칭).
make_dcl은 dcl_type과 int64 timestamp를 언패킹하고, make_timer는
int64 timestamp 하나만 언패킹한다. 유일하게 메모리를 할당하는 팔은
make_dml이다. make_dml은 두 개의 대칭 블록을 처리한다 — changed 컬럼
(새 이미지)과 cond 컬럼(이전 이미지). 각 블록: (1) 컬럼 수 언패킹;
(2) 0이 아니면 세 개의 병렬 배열(changed_column_index[] int,
changed_column_data[] char*, changed_column_data_len[] int) malloc,
각 실패 → FAILED_MALLOC; (3) 인덱스 언패킹; (4) 컬럼별로 pack_func_code를
언패킹한 뒤 switch로 in place 디코딩:
// cubrid_log_make_dml -- src/api/cubrid_log.c (changed-column value switch) ptr = or_unpack_int (ptr, &pack_func_code); switch (pack_func_code) { case 0: data[i] = ptr; or_unpack_int (...); len[i] = OR_INT_SIZE; break; // 1-4,8 analogous: int64/float/double/short/string; case 6 is assert(0), unused case 5: data[i] = ptr; or_unpack_string_nocopy (...); len[i] = strlen (data[i]); break; case 7: data[i] = ptr; or_unpack_string_nocopy (...); /* <- NULL-tolerant */ len[i] = (data[i] == NULL) ? 0 : strlen (data[i]); break; default: assert (0); }pack_func_code는 컬럼별 타입 태그다: 0=int, 1=int64/bigint, 2=float,
3=double, 4=short, 5/7/8=string (7은 NULL 허용), 6은 예약
(미사용 → assert(0)). cond 블록도 동일한 switch를 반복한다(case 1은
changed 블록이 OR_INT64_SIZE로 표기하는 같은 너비를 OR_BIGINT_SIZE로 표기).
불변 조건: DML 세 배열은 인덱스 평행(index-parallel)이며 함께 해제된다 —
clear_data_item이 셋을 하나의 단위로 해제하고 count를 0으로 초기화하므로,
이중 해제(double-clear)는 no-op이고 평행성이 깨져 double-free가 발생할 수 없다.
8.7 정리와 종료
섹션 제목: “8.7 정리와 종료”// cubrid_log_clear_log_item -- src/api/cubrid_log.c ...guards: g_stage != EXTRACTION; log_item_list NULL... for (i = 0; i < g_log_items_count; i++) /* if count == 0, no-op */ cubrid_log_clear_data_item ((DATA_ITEM_TYPE) g_log_items[i].data_item_type, &g_log_items[i].data_item); g_log_items_count = 0;clear_data_item은 DML 컬럼 배열만 해제한다. DDL/DCL/TIMER 팔은 no-op
(이들의 포인터는 blob의 별칭이다). 루프가 전달된 인수가 아닌 g_log_items[]를
순회함을 주의한다(인수는 null 검사에만 쓰인다).
flowchart TB
A["cubrid_log_finalize"] --> B{"stage가 PREPARATION/EXTRACTION?"}
B -->|no| ERR["INVALID_FUNC_CALL_STAGE"]
B -->|yes| C["disconnect_server"]
C -->|실패| H1["reset_globals; stage=CONFIGURATION; FAILED_DISCONNECT"]
C -->|성공| D["reset_globals"] --> E["g_stage = CONFIGURATION; SUCCESS"]
Figure 8-4 — cubrid_log_finalize는 disconnect 실패 경로에서도 전역 변수와
스테이지를 항상 초기화한다. 라이브러리가 사용 가능한 CONFIGURATION 상태로
복귀하며, 막히지(wedge) 않는다.
cubrid_log_disconnect_server는 NET_SERVER_CDC_END_SESSION을 전송하고
1-int 응답을 읽는다. reply_code != NO_ERROR이면 FAILED_DISCONNECT (-3)을
반환하며, 어느 경우든 g_conn_entry를 css_free_conn으로 해제한다.
cubrid_log_reset_globals는 기본값으로 복원하고(타임아웃 300, max_log_item 512),
g_extraction_table과 각 g_extraction_user[i]를 해제하고, trace log를
리셋하고, g_next_lsa = LSA_INITIALIZER로 초기화하며, g_log_infos는
의도적으로 누수한다(free가 주석 처리되어 있으며 포인터만 null로 설정됨).
8.8 반환 코드 어휘
섹션 제목: “8.8 반환 코드 어휘”| 코드 | 의미 | 생성 위치 |
|---|---|---|
SUCCESS (0) | 정상 성공. | 모든 진입점. |
SUCCESS_WITH_NO_LOGITEM (1) | 빈 배치. | 어휘만 존재 — 죽은 rc 검사; 빈 배치는 num_infos == 0으로 신호된다. |
SUCCESS_WITH_ADJUSTED_LSA (2) | 시작 LSA가 스냅(snap)됨. | 어휘만 존재 — find_start_lsa의 중복 if에 의해 은폐된다. |
INVALID_FUNC_CALL_STAGE (-27) | 잘못된 스테이지. | 가드가 있는 모든 진입점. |
EXTRACTION_TIMEOUT (-6) | 타임아웃 내 데이터 없음. | extract_internal (ER_CDC_EXTRACTION_TIMEOUT). |
INVALID_LSA (-5) | 커서 LSA가 아카이브에서 삭제됨. | extract_internal (ER_CDC_INVALID_LOG_LSA). |
UNAVAILABLE_CDC_SERVER (-34) | 서버에 supplemental_log 없음. | send_configurations (ER_CDC_NOT_AVAILABLE). |
FAILED_LOGIN (-33) | 인증 실패 또는 비-DBA 사용자. | db_login. |
FAILED_MALLOC (-28) | 메모리 할당 실패. | setter들, extract_internal, make_dml, make_log_item_list. |
8.9 챕터 요약 — 핵심 정리
섹션 제목: “8.9 챕터 요약 — 핵심 정리”- 세션 전체가
g_stage로 게이트된 전역 상태 —CONFIGURATION → PREPARATION → EXTRACTION(FINALIZATION은 흔적만 남은 값), 함수별INVALID_FUNC_CALL_STAGE로 강제. 핸들 없음. - setter는 전역 변수만 쓴다; 네트워크는
send_configurations에서 한 번만 접근한다. 따라서 스트림 도중 필터 경쟁 조건이 없다. - 연결은 세 단계 합성:
db_login(throwawaydb_restart로 DBA 강제) →connect_server_internal(CSS 핸드셰이크, Windows 2단계 포트 핸드오프) →send_configurations. 모두 성공해야g_stage가 전진한다. - 추출은 2-요청:
…_METADATA가next_lsa | num_infos | total_length를 반환하고 grow-onlyrealloc으로g_log_infos를 키운다;GET_LOGINFO가 blob을 채운다(length 0이면 생략). 소비자가 in/outlsa로 커서를 소유한다. - 디코딩은 blob을 별칭한다 (
or_unpack_string_nocopy).make_dml만 세 개의 인덱스 병렬 컬럼 배열을 할당하며,clear_data_item이 이를 하나의 단위로 해제한다. pack_func_code는 컬럼별 타입 태그 (0=int, 1=int64, 2=float, 3=double, 4=short, 5/7/8=string, 7은 NULL 허용, 6은 예약).SUCCESS_WITH_NO_LOGITEM과SUCCESS_WITH_ADJUSTED_LSA는 ABI 어휘에만 존재하며 실제로는 생성되지 않는다 (죽은 분기).finalize는 disconnect 실패 시에도 항상CONFIGURATION으로 초기화하며,g_log_infos는 의도적으로 누수된다.
Chapter 9: 엣지 경로 — 레거시 Applier와 공유 로그 리더
섹션 제목: “Chapter 9: 엣지 경로 — 레거시 Applier와 공유 로그 리더”2~8장은 현대 CDC walker의 정상 경로를 추적했다. 이 장은 그 생명주기 바깥에 위치한 코드를 세 묶음으로 정리한다.
- 9.1–9.3
log_reader공유 전방 WAL walker — 그리고 CDC가 이것을 사용하지 않는다는 놀라운 사실. - 9.4 디스패처가 반드시 처리해야 하는 엣지 이벤트: 취소된 abort, keep-alive 레코드, 다중 구문 DDL, 필터 경쟁 조건.
- 9.5–9.8 레거시
la_*HA 데몬(applylogdb) — 동일한 WAL을 읽되 물리적UNDOREDO레코드로부터 재구성하여 슬레이브 서버에 SQL을 재생하는 방식, 슬레이브 측 SQL 감사 로그 포함.
“왜 두 파이프라인이 존재하는가”라는 설계 수준의 이유는 cubrid-cdc.md를 참고한다. 이 장은 이론이 아닌 코드를 추적한다.
9.1 log_reader — 공유 전방 walker
섹션 제목: “9.1 log_reader — 공유 전방 walker”log_reader(log_reader.hpp)는 정렬된 로그 페이지 하나와 커서 LSA를 소유하는 작은 RAII 래퍼다. WAL을 앞 방향으로 바이트 단위로 fetch·정렬·복사하는 메서드를 제공하며, 현재 log_recovery가 사용하는 표준 순차 읽기 추상화다.
classDiagram
class log_reader {
-THREAD_ENTRY* m_thread_entry
-log_lsa m_lsa
-LOG_CS_ACCESS_MODE m_cs_access
-log_page* m_page
-char m_area_buffer[IO_MAX_PAGE_SIZE+DOUBLE_ALIGNMENT]
+set_lsa_and_fetch_page(lsa, fetch_mode)
+align() add_align() advance_when_does_not_fit()
+does_fit_in_current_page() copy_from_log() skip()
}
log_reader --> log_page : m_page는 m_area_buffer를 가리킴
Figure 9-1 — log_reader는 자체 페이지 버퍼를 소유한다. m_page는 m_area_buffer를 MAX_ALIGNMENT 경계로 재정렬한 포인터다: 공유 버퍼 없음, 잠금 없음, 완전히 독립적.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
m_thread_entry | 캐시된 THREAD_ENTRY*, 지연 초기화 | 단일 스레드 전제를 인코딩한다. |
m_lsa | 커서(pageid, offset) | walk 상태. advance/align/skip이 이 값을 변경한다. |
m_cs_access | fetch 시 사용할 LOG_CS_ACCESS_MODE | log critical section 취득 여부를 결정한다. |
m_page | m_area_buffer에 정렬된 활성 페이지 | 커서가 읽는 디코딩된 페이지. |
m_area_buffer | IO_MAX_PAGE_SIZE + DOUBLE_ALIGNMENT 바이트 | 여유 공간으로 m_page를 경계에 맞춰 슬라이드할 수 있다. |
생성자에서 정렬이 이루어진다:
// log_reader::log_reader -- src/transaction/log_reader.hpplog_reader::log_reader (LOG_CS_ACCESS_MODE cs_access) : m_cs_access (cs_access){ m_page = reinterpret_cast<log_page *> (PTR_ALIGN (m_area_buffer, MAX_ALIGNMENT)); /* <- 버퍼 내부로 슬라이드 */}INVARIANT — 하나의 walker, 하나의 스레드. 헤더에 명시되어 있다:
NOTE: not thread safe.m_thread_entry는 첫 사용 시 캐시되고, 모든 변경 메서드는assert (thread_p == &cubthread::get_entry ())를 수행한다. 계약은 각 walker가 자신의 인스턴스를 소유하는 것이다. 두 스레드가 하나의log_reader를 공유하면, 캐시된m_thread_entry는 먼저 접근한 스레드의 것이 되고,m_lsa/m_page는 경쟁 상태에 빠지며, 페이지 버퍼는 조용히 오염된다. CDC는 데몬별 전용 버퍼(9.3)로 이 계약을 지키고, 레거시 applier는 단일 스레드 구조로 이를 지킨다.
fetch_mode는 언제 페이지를 다시 읽을지를 표현한다. FORCE는 LETS 포팅 잔재로, recovery는 NORMAL만 사용한다. 향후 재사용을 위해 유지 중: enum class fetch_mode { NORMAL, FORCE };.
9.2 Fetch, 정렬, 경계 헬퍼 — 분기별 분석
섹션 제목: “9.2 Fetch, 정렬, 경계 헬퍼 — 분기별 분석”**set_lsa_and_fetch_page**는 디스크 fetch가 필요한지 판단한다. 분기 A: do_fetch_page가 true인 경우(호출자가 FORCE를 지정했거나 목표 pageid가 현재 페이지와 다를 때) → logpb_fetch_page 한 번 호출. 분기 B: false인 경우(LSA가 이미 m_page의 페이지에 속할 때) → m_lsa만 이동하고 I/O 없음. “pageid가 바뀔 때만 재fetch”가 핵심 성능 포인트다 — 같은 페이지 내의 반복적인 offset 이동은 비용이 없다.
// log_reader::set_lsa_and_fetch_page -- src/transaction/log_reader.hppconst bool do_fetch_page { fetch_page_mode == fetch_mode::FORCE || m_lsa.pageid != lsa.pageid }; /* <- 페이지 변경 시에만 재fetch */m_lsa = lsa;if (do_fetch_page) { ... return fetch_page (get_thread_entry ()); }return NO_ERROR;**fetch_page**는 I/O가 일어나는 유일한 지점이며, 실패 시 치명적이다 — 전방 walk 중 fetch 실패는 WAL을 읽을 수 없다는 의미이므로 logpb_fatal_error로 에스컬레이션한다. 뒤따르는 return ER_FAILED는 방어적 코드다(fatal 호출은 실제로 반환하지 않는다):
// log_reader::fetch_page -- src/transaction/log_reader.hppif (logpb_fetch_page (thread_p, &m_lsa, m_cs_access, m_page) != NO_ERROR) { logpb_fatal_error (thread_p, true, ARG_FILE_LINE, "log_reader::fetch_page"); return ER_FAILED; }정렬 헬퍼들은 커서가 LOGAREA_SIZE를 넘을 때 다음 페이지를 fetch하는 패턴을 공유하는 세 개의 free function을 감싼다:
// LOG_READ_ALIGN -- src/transaction/log_reader.hpplsa->offset = DB_ALIGN (lsa->offset, DOUBLE_ALIGNMENT); /* <- 8B 단위로 올림 */while (lsa->offset >= (int) LOGAREA_SIZE) { /* <- 페이지 끝을 넘어섰는가? */ lsa->pageid++; if (logpb_fetch_page (...) != NO_ERROR) logpb_fatal_error (...); lsa->offset -= LOGAREA_SIZE; /* <- 나머지를 새 페이지로 이월 */ lsa->offset = DB_ALIGN (lsa->offset, DOUBLE_ALIGNMENT);}LOG_READ_ALIGN(align):offset을DOUBLE_ALIGNMENT로 올림.while(if가 아님)은 다중 페이지 오버런을 처리하며, 페이지마다 fetch와LOGAREA_SIZE빼기를 반복한다.LOG_READ_ADD_ALIGN(add_align):offset += add후LOG_READ_ALIGN.reinterpret_copy_and_add_align<T>()가 struct를 건너뛰고 한 번에 재정렬할 때 사용한다.LOG_READ_ADVANCE_WHEN_DOESNT_FIT(advance_when_does_not_fit): 단순if하나 —offset + length >= LOGAREA_SIZE이면 레코드가 경계에 걸리는 것이므로,pageid++, fetch(실패 시 fatal),offset = 0으로 새 페이지에서 레코드 전체를 다시 시작한다. 페이지 꼬리는 건너뛴다.
does_fit_in_current_page(size)는 부작용 없는 조건자(m_lsa.offset + size < LOGAREA_SIZE)로, 인플레이스 읽기와 copy_from_log 중 어느 쪽을 쓸지 결정한다. copy_from_log(dest, length)는 logpb_copy_from_log에 위임하며 내부적으로 straddle 상황에서 페이지를 전진시킨다. skip(size): 분기 (1) offset + size < LOGAREA_SIZE → offset += size; 분기 (2) while로 페이지마다 size를 소진하며 fetch, offset = 0 초기화, 각 페이지 래핑 후 align() 재정렬.
9.3 CDC의 이탈: CDC는 log_reader를 사용하지 않는다
섹션 제목: “9.3 CDC의 이탈: CDC는 log_reader를 사용하지 않는다”CDC를 수정하는 누구에게나 핵심이 되는 교차 검증 사항: log_reader가 범용 전방 walker임에도 불구하고, 현대 CDC 프로듀서는 이를 인스턴스화하지 않는다. CDC는 cdc_Gl.producer.temp_logbuf[0..1]에 있는 전용 두 슬롯 캐시를 유지하는 log_impl.h의 세 개 매크로로 페이지를 읽는다:
// CDC_GET_TEMP_LOGPAGE -- src/transaction/log_impl.hif (cdc_Gl.producer.temp_logbuf[(process_lsa)->pageid % 2].log_page_p->hdr.logical_pageid != (process_lsa)->pageid) { if (logpb_fetch_page (..., LOG_CS_FORCE_USE, (log_page_p)) != NO_ERROR) { goto error; } memcpy (cdc_Gl.producer.temp_logbuf[(process_lsa)->pageid % 2].log_page_p, (log_page_p), IO_MAX_PAGE_SIZE);} else { (log_page_p) = cdc_Gl.producer.temp_logbuf[(process_lsa)->pageid % 2].log_page_p; }pageid % 2로 인덱싱하는 이 캐시는 현재 페이지와 인접 페이지를 동시에 유지한다 — 6장의 undo/redo 이중 LSA 재구성에 정확히 필요한 것이며, 단일 페이지 log_reader로는 제공할 수 없는 기능이다. 두 슬롯은 시작 시 정렬된다. CDC_CHECK_TEMP_LOGPAGE는 홀짝(parity)이 바뀔 때 활성 슬롯을 교체하고, CDC_UPDATE_TEMP_LOGPAGE는 쓰기로 인해 확장되었을 수 있는 캐시된 페이지를 재fetch한다.
이탈 주의.
cubrid-cdc.md가 프로듀서를 “공유 로그 리더를 사용”한다고 기술한다면, 그것은 포부적 설명이다: 이 리비전 기준으로 프로듀서는 자체 두 페이지 버퍼와 함께CDC_*_TEMP_LOGPAGE를 사용한다.log_reader를 채택하려는 편집자는 먼저temp_logbuf[pageid % 2]홀짝 트릭을 보존하는 두 페이지 버퍼링 모드를 추가해야 한다. 이중 LSA 재구성은 undo 페이지와 redo 페이지가 동시에 메모리에 있어야 하기 때문이다.
9.4 디스패처가 반드시 처리해야 하는 엣지 이벤트
섹션 제목: “9.4 디스패처가 반드시 처리해야 하는 엣지 이벤트”단일 변경 흐름 외에도, la_log_record_process(와 이를 미러링하는 현대 디스패처)는 하위 시스템에 변경을 전혀 전달하지 않는 레코드 종류들을 흡수해야 한다.
Abort는 재구성되지 않고 버려진다. la_apply_repl_log는 LOG_ABORT를 만나면 즉시 종료한다: 수집된 LA_ITEM 목록을 해제하고 LSA 부기를 앞으로 진행시킨다. 현대 CDC도 마찬가지로 abort된 트랜잭션의 행을 절대 표면에 드러내지 않는다.
// la_apply_repl_log -- src/transaction/log_applier.cif (rectype == LOG_ABORT) { la_clear_applied_info (apply); return NO_ERROR; } /* <- 아이템 목록 폐기 */LOG_DUMMY_HA_SERVER_STATE는 keep-alive / 역할 변경 신호로, 행 데이터를 전혀 담지 않는다. 세 가지 결과: (1) dbname 락을 보유한 상태에서 ACTIVE/TO_BE_STANDBY가 아닌 상태로 변경 → is_role_changed 설정 후 ER_INTERRUPTED 반환으로 외부 루프가 락을 해제하도록 함; (2) 상태가 active이고 대기 중인 repl 목록이 없음 → at_time 기록 및 la_log_commit(true) 호출로 _db_ha_apply_info에 하트비트 flush; (3) 그 외는 fall through.
// la_log_record_process, LOG_DUMMY_HA_SERVER_STATE -- src/transaction/log_applier.celse if (la_is_repl_lists_empty ()) { (void) la_update_ha_apply_info_log_record_time (ha_server_state->at_time); error = la_log_commit (true); /* <- keep-alive 커밋 */}다중 구문 DDL은 la_apply_statement_log의 case 연쇄에서 처리된다(9.6): CUBRID_STMT_CREATE_CLASS 아이템은 구문 실행 외에 GRANT ALL PRIVILEGES ... TO <db_user>도 함께 실행하여 객체가 올바른 소유자와 함께 생성되도록 한다. SQL 로깅 측에서는 ha_sys_prm을 담은 DDL을 SET SYSTEM PARAMETERS 전처리/후처리로 감싸, 논리적 변경 하나가 세 개의 구문으로 기록된다.
필터 재설정 경쟁 조건. 필터(LA_REPL_FILTER, 9.7)는 핫 패스에서 la_need_filter_out이 읽지만, 시작 시 la_create_repl_filter에서 한 번만 구성된다. filter->list / filter->num_filters 주변에 락이 없다. 설계는 필터가 applier 생명주기 동안 불변임을 가정하므로, ha_repl_filter_file을 변경하려면 재시작이 필요하다. 라이브 재설정을 추가하려면 이 지점에 동기화가 필요하다.
9.5 레거시 la_* HA 데몬 — 외부 루프
섹션 제목: “9.5 레거시 la_* HA 데몬 — 외부 루프”la_apply_log_file은 applylogdb의 전체 메인 루프다 — 현대 프로듀서 데몬(5장)의 레거시 대응물이지만, 물리적 UNDOREDO 레코드를 읽어 라이브 슬레이브에 SQL로 재생한다.
flowchart TB
init["la_init + sl_init + la_init_cache_pb\nla_find_log_pagesize\nla_get_last_ha_applied_info"] --> filt["설정된 경우 REPL_FILTER 구성"]
filt --> outer{"do: la_applier_need_shutdown?"}
outer -->|"la_apply_pre가 final_lsa 설정"| inner{"while final_lsa가 null이 아닌 동안"}
inner --> arv["오래된 아카이브 삭제\nfinal_log_hdr 갱신"]
arv --> state{"서버 상태 ACTIVE?"}
state -->|"DONE이고 active 아님"| relock["dbname 락 해제, log_commit, RECOVERING, sleep"]
state -->|"페이지 미기록"| sleep1["usleep, continue"]
state -->|"정상"| getpage["la_get_page_buffer(final_lsa.pageid)"]
getpage -->|"NULL"| retry{"retry < LA_GET_PAGE_RETRY_COUNT?"}
retry -->|"예"| sleep1
retry -->|"아니오"| corrupt["ER_LOG_PAGE_CORRUPTED, 종료"]
getpage -->|"페이지 정상"| recloop["레코드별: la_log_record_process"]
recloop --> commit["la_check_time_commit\nla_check_mem_size\nla_change_state"]
commit --> inner
inner --> outer
outer -->|"종료"| done["reinit_copylog? la_shutdown, error 반환"]
Figure 9-2 — la_apply_log_file 제어 흐름, 분기별. 설정: la_init(9.7), 선택적 sl_init, la_check_duplicated(로그 경로당 applier 하나), 캐시/페이지크기/recdes-풀 초기화, 이후 la_get_last_ha_applied_info가 _db_ha_apply_info에서 재시작 지점을 읽음. 필터: PRM_ID_HA_REPL_FILTER_TYPE != REPL_FILTER_NONE일 때만 구성되며, 재생은 required_lsa에서 시작. 아카이브 GC: remove_arv_interval_in_secs마다 실행, 또는 간격이 0이면 nxarv_num이 증가할 때만. 강등(applier DONE, 마스터가 ACTIVE/TO_BE_STANDBY 아님): dbname 락 해제, la_log_commit(false), RECOVERING으로 전환, sleep, continue. 페이지 존재 여부: eof_lsa.pageid < final_lsa.pageid → 짧은 sleep; la_get_page_buffer NULL → 미동기/append_lsa 초과/손상 구분, retry_count 한도 초과 시 ER_LOG_PAGE_CORRUPTED; hdr.offset이 음수이면 torn page로, 다음 페이지가 존재하면 건너뛰고 아니면 재fetch. 내부 레코드 루프: 0 오프셋 보정, eof_lsa/append_lsa 대비 로그 끝 확인, prev_final == lrec->back_lsa 검증(체인 무결성, 불일치 시 fatal), 이후 la_log_record_process; 에러 분기는 서버 다운(ER_NET_CANT_CONNECT_SERVER), 메모리 초과/flush(종료), 페이지 손상(무효화, 재fetch)을 구분. 종료: 페이지별 la_check_time_commit / la_check_mem_size / la_change_state; reinit_copylog 시 _db_ha_apply_info 삭제 후 강제 에러; 항상 la_shutdown.
9.6 레코드별 디스패치 — la_apply_repl_log와 la_apply_statement_log
섹션 제목: “9.6 레코드별 디스패치 — la_apply_repl_log와 la_apply_statement_log”la_apply_repl_log는 LOG_COMMIT / LOG_SYSOP_END / LOG_ABORT 시점에 실행된다. abort와 “처리할 것 없음” 조기 종료 이후, dbname 락을 획득하고 트랜잭션의 LA_ITEM 목록을 순회한다. last_committed_rep_lsa 이후의 항목 중 필터에 걸리지 않은 것은: LOG_REPLICATION_DATA는 item_type으로 분기(RVREPL_DATA_UPDATE*/INSERT/DELETE → la_apply_{update,insert,delete}_log; 그 외는 assert_release(false)); LOG_REPLICATION_STATEMENT → la_apply_statement_log. 에러 분기: flush 에러 → goto end; 서버 다운 → ER_NET_CANT_CONNECT_SERVER, goto end; 그 외 재시도 가능하고 무시 불가 → LA_SLEEP(10,0) 후 continue(동일 아이템 재시도). 재시도 대상은 LA_RETRY_ON_ERROR(락/페이지 래치 타임아웃, 데드락, TDE 암호 미로드) 및 la_retry_on_error를 통한 사용자 지정 ha_applylogdb_retry_error_list다.
la_apply_statement_log는 DDL/구문 경로다. 상단의 la_flush_repl_items(true)는 핵심 순서 장벽이며, is_ddl fallthrough와 DROP_LABEL/default 허용 분기가 나머지 두 가지 비자명한 분기다:
// la_apply_statement_log -- src/transaction/log_applier.cerror = la_flush_repl_items (true); /* <- 대기 중인 DML 먼저 소진; 순서 장벽 */if (error != NO_ERROR) return error;switch (item->item_type) { case CUBRID_STMT_CREATE_CLASS: ... case CUBRID_STMT_UPDATE_STATS: is_ddl = true; /* FALLTHROUGH into the execute arm */ case CUBRID_STMT_INSERT: ... case CUBRID_STMT_UPDATE: /* 사용자 설정, SQL 로그 기록, sysprm 설정, 실행, 복원 */ break; case CUBRID_STMT_DROP_LABEL: default: return NO_ERROR; /* <- 알 수 없거나 구식 구문은 조용히 무시 */}이 장벽은 스키마 변경 전에 대기 중인 DML이 슬레이브에 적용되도록 보장한다. 그렇지 않으면 잘못된 스키마에 DML이 실행된다. TRUNCATE는 DML 형태의 구문 중 switch 중간에서 필터를 존중하는 유일한 것이다. DDL은 schema_counter를 증가시키고, DML은 영향받은 행 수 res만큼 insert/update/delete 카운터를 증가시키며, 최종 에러는 fail_counter를 증가시키고 ER_HA_LA_FAILED_TO_APPLY_STATEMENT를 발생시킨다. DROP_LABEL/default 분기는 구식 레코드 타입을 충돌 없이 허용하기 위해 NO_ERROR를 반환한다.
9.7 재시작 부기와 초기화
섹션 제목: “9.7 재시작 부기와 초기화”**LA_HA_APPLY_INFO**는 _db_ha_apply_info 카탈로그 행의 메모리 이미지다 — applylogdb가 중단된 지점에서 재시작할 수 있게 해주는 영속적 레코드.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
db_name | 슬레이브 DB 이름 | 카탈로그 행 식별자. |
creation_time | 마스터 생성 타임스탬프 | 재구축된 피어 탐지. |
copied_log_path | 복사된 로그 볼륨 경로 | WAL 위치 확인. |
committed_lsa | 마지막 커밋된 commit-log LSA | 커밋 재생 재시작 지점. |
committed_rep_lsa | 마지막 커밋된 replication-log LSA | 아이템 재생 재시작 지점. |
append_lsa / eof_lsa | 마스터 append / EOF 스냅샷 | 지연 및 레코드 끝 탐지. |
final_lsa | 마지막 처리된 LSA | 스캔 커서. |
required_lsa | 가장 오래된 미완료 트랜잭션의 시작 LSA | 보존 경계; 아카이브 GC를 결정. |
log_record_time / log_commit_time / last_access_time / start_time | 타임스탬프 | applyinfo를 통한 하트비트 / 지연 측정. |
status | IDLE / BUSY | 현재 재생 중 플래그. |
insert/update/delete/schema/commit/fail_counter (INT64) | 누적 카운터 | 재시작을 넘나드는 메트릭. |
la_init_ha_apply_info는 단순한 초기화다: memset 후 여섯 개의 LSA 각각에 LSA_SET_NULL — zeroed struct는 null LSA가 아니므로 명시적 nulling이 중요하다.
// la_init_ha_apply_info -- src/transaction/log_applier.cmemset ((void *) ha_apply_info, 0, sizeof (LA_HA_APPLY_INFO));LSA_SET_NULL (&ha_apply_info->committed_lsa); /* ... + 5개 LSA 더 ... */INVARIANT — 카탈로그 행이 재시작의 진실 원천이다.
la_get_last_ha_applied_info는 시작 시_db_ha_apply_info를la_Info로 로드하고,la_log_commit은 트랜잭션적으로 다시 기록한다(la_update_ha_last_applied_info후la_commit_transaction). 불변 조건은 인메모리committed_lsa를 카탈로그에 내구성 있게 기록된 것 이상으로 절대 진행시키지 않는 것이다. 이를 위반하면 크래시 후 트랜잭션이 조용히 건너뛰어진다.
la_log_commit — 분기 추적(update_commit_time 불리언이 호출자가 keep-alive와 강등을 구분하는 이유다):
// la_log_commit -- src/transaction/log_applier.c(void) la_find_required_lsa (&la_Info.required_lsa);LSA_COPY (&la_Info.append_lsa, ...->append_lsa); LSA_COPY (&la_Info.eof_lsa, ...->eof_lsa); /* (1) 항상 스냅샷 */if (update_commit_time) { la_Info.log_commit_time = time (0); } /* (2) true일 때만 기록 */error = la_flush_repl_items (true);if (error != NO_ERROR) { return error; } /* (3) flush 실패 -> 카탈로그 쓰기 없음 */res = la_update_ha_last_applied_info ();if (res > 0) { error = la_commit_transaction (); } /* (4a) 기록 성공 -> 커밋 */else { la_Info.fail_counter++; if (ER_IS_SERVER_DOWN_ERROR (res)) error = ER_NET_CANT_CONNECT_SERVER; /* (4b) 전파 */ else { er_set (...); error = NO_ERROR; } /* (4c) 무시; 재생이 재수행 */}분기는 인라인으로 (1)–(4c)로 표기된다: (2)는 keep-alive 호출자에서만 log_commit_time을 기록하므로, 강제 강등 커밋은 하트비트 시계를 진행시키지 않는다; (3)은 카탈로그 쓰기 전에 반환한다; (4c)는 서버 다운이 아닌 에러를 NO_ERROR로 삼켜서 마지막으로 영속화된 LSA부터 재생이 재수행되도록 한다.
**la_init**은 la_Info 전역(카탈로그가 아님)을 초기화한다: memset, 로그 경로 복사, 기본 페이지/캐시 크기(LA_DEFAULT_*) 설정, 두 로그 볼륨 디스크립터를 NULL_VOLDES로 표시, 여덟 개의 작업 LSA 모두 null 설정, last_deleted_archive_num = -1 설정, start_vsize/start_time 캡처(la_check_mem_size 기준선), DB_CLIENT_TYPE_LOG_APPLIER용 replication 객체 워크스페이스 초기화, repl_filter.type = REPL_FILTER_NONE 설정. 캐시 버퍼나 recdes 풀은 할당하지 않는다 — 이는 로그 헤더 페이지 크기가 확인된 후 la_apply_log_file에서 이루어진다.
**REPL_FILTER_TYPE**과 컨테이너 LA_REPL_FILTER:
// REPL_FILTER_TYPE -- src/transaction/log_applier.htypedef enum { REPL_FILTER_NONE, REPL_FILTER_INCLUDE_TBL, REPL_FILTER_EXCLUDE_TBL } REPL_FILTER_TYPE;| 값 | 의미 | la_need_filter_out 결과 |
|---|---|---|
REPL_FILTER_NONE | 필터링 없음 | 항상 false |
REPL_FILTER_INCLUDE_TBL | 화이트리스트 | 클래스가 list에 없으면 필터 아웃 |
REPL_FILTER_EXCLUDE_TBL | 블랙리스트 | 클래스가 list에 있으면 필터 아웃 |
LA_REPL_FILTER 필드: list(char** 테이블 이름), list_size(할당된 용량), num_filters(사용 중인 수), type. la_need_filter_out은 item->class_name에서 [...] 괄호를 제거하고, 구문 복제(단 TRUNCATE 제외)와 _db_serial 시스템 클래스를 면제한 뒤, list를 대소문자 구분 없이 선형 탐색한다.
9.8 LA_ITEM과 슬레이브 측 SQL 감사 로그
섹션 제목: “9.8 LA_ITEM과 슬레이브 측 SQL 감사 로그”**LA_ITEM**은 대기 중인 복제 변경 하나를 나타낸다 — 현대 per-change 이벤트(6장)의 레거시 대응물.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
next / prev | 리스트 포인터 | 트랜잭션별 체인, LSA 순서대로 재생. |
log_type | LOG_REPLICATION_DATA / ..._STATEMENT | 최상위 디스패치 키. |
item_type | RVREPL_DATA_* 또는 CUBRID_STMT_* | 2단계 디스패치. |
class_name | 대상 테이블 | 필터 매칭, SQL 생성. |
db_user | 가장할 소유자 | AU_SET_USER를 통한 DDL. |
ha_sys_prm | HA 시스템 파라미터 | DDL을 SET SYSTEM PARAMETERS로 감싸기. |
packed_key_value / packed_key_value_length | PK의 디스크 이미지 | key로 지연 언팩. |
key | 언팩된 PK DB_VALUE | update/delete의 WHERE 기준; 구문의 경우 구문 텍스트. |
lsa | replication-log 레코드의 LSA | 순서 지정, committed_rep_lsa 커서. |
target_lsa | 대상 data-log 레코드의 LSA | 물리적 UNDOREDO 레코드 위치. |
슬레이브 측 SQL 감사 로그(log_applier_sql_log.c)는 포렌식을 위해 적용된 모든 구문을 재생 가능한 SQL로 선택적으로 기록한다. 전역 sl_Info는 curr_file_id와 last_inserted_sql_id만 보유하며, FILE* 핸들(log_fp, catalog_fp), sql_log_max_cnt, 경로 버퍼는 별도의 파일 정적 전역 변수다.
sl_write_sql — 구문별 기록기. 분기: 지연 열기; -- 메타데이터 헤더 출력(타임스탬프, 단조 증가 sql_id, select 길이, 쿼리 길이); 선택적 검증 SELECT; 쿼리 기록; flush; sl_write_catalog로 커서 영속화; SL_LOG_FILE_MAX_SIZE 초과 시 롤오버하며 가장 오래된 파일 정리.
// sl_write_sql -- src/transaction/log_applier_sql_log.cif (log_fp == NULL) { if ((log_fp = sl_log_open ()) == NULL) return ER_FAILED; }fprintf (log_fp, "-- %s | %u | %zu | %zu\n", time_buf, ++sl_Info.last_inserted_sql_id, ...); /* <- 헤더 */fwrite (query.get_buffer (), sizeof (char), query.len (), log_fp); fflush (log_fp);sl_write_catalog ();if (ftell (log_fp) >= SL_LOG_FILE_MAX_SIZE) { log_fp = sl_open_next_file (log_fp); sl_delete_oldest_file_if_needed (); }**sl_log_open**은 <base>.<curr_file_id>를 연다: 존재하면 "r+"(크기 초과 시 롤 전진), 없으면 "w"로 생성; 실패 시 NULL 반환(호출자 → ER_FAILED).
**sl_read_catalog**는 시작 시 롤링 파일 커서를 복원한다 — 세 분기: 카탈로그 없음 → sl_write_catalog로 부트스트랩; 읽을 수 없는 줄 → ER_FAILED; 잘못된 형식(두 필드가 아님) → ER_FAILED:
// sl_read_catalog -- src/transaction/log_applier_sql_log.cread_catalog_fp = fopen (sql_catalog_path, "r");if (read_catalog_fp == NULL) return sl_write_catalog (); /* <- 카탈로그 없음: 생성 */if (fgets (info, LINE_MAX, read_catalog_fp) == NULL) { ...; return ER_FAILED; }if (sscanf (info, CATALOG_FORMAT, &sl_Info.curr_file_id, &sl_Info.last_inserted_sql_id) != 2) { fclose (read_catalog_fp); return ER_FAILED; } /* <- 손상된 카탈로그 */"%u | %010u" 형식(curr_file_id, last_inserted_sql_id)이 재시작 사이에 감사 로그가 번호를 재개할 수 있게 해주는 단 한 줄이다.
레거시가 스키마에 취약하고 현대 CDC는 그렇지 않은 이유. 레거시 applier는 물리적
UNDOREDO레코드에서 각 변경을 재구성한다. 재생 시점에 슬레이브의 클래스 레이아웃, 속성 순서, PK 제약을 알아야 한다(sl_print_pk가classobj_find_class_primary_key를 순회하고, 9.6의 DDL 소유자 재작성에 주목). 슬레이브 스키마가 마스터에서 벗어나면 — 대역 외 컬럼 추가, 다른 속성 순서 — 재구성이 잘못되거나 거부된 SQL을 생성한다. 현대 CDC는 이를 우회한다: 프로듀서가 컬럼 이름과 타입이 포함된 보충(supplemental) 레코드를 내보내므로(2장, 6장, 7장), 소비자는 물리적 레이아웃을 역공학할 필요가 없다. 이것이 두 파이프라인이 공존하는 핵심 이유다. 설계 근거는cubrid-cdc.md를 참고한다.
9.9 챕터 요약 — 핵심 정리
섹션 제목: “9.9 챕터 요약 — 핵심 정리”log_reader는 프로젝트의 전방 WAL walker이지만, CDC는 이중 LSA 재구성을 위해 두 페이지가 동시에 메모리에 있어야 하므로CDC_*_TEMP_LOGPAGE로 자체 두 슬롯temp_logbuf[pageid % 2]캐시를 유지한다 — “CDC가 log_reader를 사용한다”는 주장은 포부적 설명으로 취급해야 한다.set_lsa_and_fetch_page는 pageid 변경 시(또는FORCE시)에만 I/O를 수행하고, 정렬 헬퍼는LOGAREA_SIZE오버런 시 fetch하며(logpb_fatal_error로 fatal), 이 클래스는 계약상 스레드 안전하지 않다 — 각 walker는 자신의 인스턴스를 소유하며, 캐시된m_thread_entry에 대한 assert로 보장된다.- Abort는 아이템 목록을 해제하고 앞으로 진행한다. 유휴
LOG_DUMMY_HA_SERVER_STATE는la_log_commit(true)로 하트비트를 커밋하고, 역할 변경은ER_INTERRUPTED를 반환하여 락을 해제한다. la_apply_log_file은 재시도 한도가 있는 fetch, 체인 무결성 확인(prev_final == back_lsa), 재시도/종료/손상을 구분하는 에러 분기를 갖춘 강건한 방어적 페이지 루프다.la_log_commit은 항상append_lsa/eof_lsa를 스냅샷하고,update_commit_time이 true일 때만(keep-alive=true, 강등=false)log_commit_time을 기록하며, flush 에러 시 조기 반환하고, 카탈로그 쓰기 실패 시 서버 다운이 아닌 에러를 삼켜서 마지막으로 내구성 있게 기록된 LSA부터 재생이 재수행되도록 한다 — 재시작 안전성 불변 조건.- 레거시 applier는 물리적
UNDOREDO레코드에서 재구성하고 SQL을 재생(소유자/sysprm 재작성, 선택적sl_write_sql감사 추적)하므로 스키마에 취약한 반면, 현대 보충 레코드 기반 CDC는 컬럼 이름이 있는 타입화된 값을 전달하여 슬레이브 측 레이아웃 변동에 영향을 받지 않는다.
이 리비전 시점의 위치 힌트
섹션 제목: “이 리비전 시점의 위치 힌트”아래 줄 번호는 2026-06-23 시점에 관찰한 값이다. 심볼 이름이 정식 앵커이며 줄 번호는 시간이 지나면 어긋나는 힌트다.
| Symbol | File | Line |
|---|---|---|
CUBRID_LOG_STAGE | src/api/cubrid_log.c | 85 |
DATA_ITEM_TYPE | src/api/cubrid_log.c | 93 |
cubrid_log_set_connection_timeout | src/api/cubrid_log.c | 158 |
cubrid_log_set_extraction_timeout | src/api/cubrid_log.c | 181 |
cubrid_log_set_tracelog | src/api/cubrid_log.c | 406 |
cubrid_log_set_max_log_item | src/api/cubrid_log.c | 457 |
cubrid_log_set_all_in_cond | src/api/cubrid_log.c | 480 |
cubrid_log_set_extraction_table | src/api/cubrid_log.c | 504 |
cubrid_log_set_extraction_user | src/api/cubrid_log.c | 535 |
cubrid_log_connect_server_internal | src/api/cubrid_log.c | 566 |
cubrid_log_send_configurations | src/api/cubrid_log.c | 686 |
cubrid_log_db_login | src/api/cubrid_log.c | 797 |
cubrid_log_connect_server | src/api/cubrid_log.c | 851 |
cubrid_log_find_start_lsa | src/api/cubrid_log.c | 929 |
cubrid_log_find_start_lsa | src/api/cubrid_log.c | 980 |
cubrid_log_find_lsa | src/api/cubrid_log.c | 1032 |
cubrid_log_extract_internal | src/api/cubrid_log.c | 1083 |
cubrid_log_make_ddl | src/api/cubrid_log.c | 1243 |
cubrid_log_make_dml | src/api/cubrid_log.c | 1262 |
cubrid_log_make_dcl | src/api/cubrid_log.c | 1492 |
cubrid_log_make_timer | src/api/cubrid_log.c | 1507 |
cubrid_log_make_data_item | src/api/cubrid_log.c | 1521 |
cubrid_log_make_log_item | src/api/cubrid_log.c | 1576 |
cubrid_log_make_log_item_list | src/api/cubrid_log.c | 1608 |
cubrid_log_extract | src/api/cubrid_log.c | 1679 |
cubrid_log_clear_data_item | src/api/cubrid_log.c | 1736 |
cubrid_log_clear_log_item | src/api/cubrid_log.c | 1791 |
cubrid_log_disconnect_server | src/api/cubrid_log.c | 1829 |
cubrid_log_reset_globals | src/api/cubrid_log.c | 1899 |
cubrid_log_finalize | src/api/cubrid_log.c | 1949 |
DDL | src/api/cubrid_log.h | 71 |
ddl | src/api/cubrid_log.h | 72 |
DML | src/api/cubrid_log.h | 82 |
dml | src/api/cubrid_log.h | 83 |
DCL | src/api/cubrid_log.h | 97 |
dcl | src/api/cubrid_log.h | 98 |
TIMER | src/api/cubrid_log.h | 104 |
timer | src/api/cubrid_log.h | 105 |
CUBRID_DATA_ITEM | src/api/cubrid_log.h | 110 |
cubrid_data_item | src/api/cubrid_log.h | 111 |
CUBRID_LOG_ITEM | src/api/cubrid_log.h | 119 |
cubrid_log_item | src/api/cubrid_log.h | 120 |
ER_CDC_IGNORE_LOG_INFO_INTERNAL | src/base/error_code.h | 1654 |
ER_CDC_IGNORE_TRANSACTION | src/base/error_code.h | 1655 |
ER_CDC_NULL_EXTRACTION_LSA | src/base/error_code.h | 1659 |
ER_CDC_LOGINFO_ENTRY_GENERATED | src/base/error_code.h | 1660 |
ER_CDC_IGNORE_LOG_INFO | src/base/error_code.h | 1661 |
scdc_find_lsa | src/communication/network_interface_sr.cpp | 11258 |
scdc_get_loginfo_metadata | src/communication/network_interface_sr.cpp | 11314 |
recdes | src/storage/storage_common.h | 220 |
REGISTER_DAEMON | src/thread/thread_manager.hpp | 498 |
log_Zip_min_size_to_compress | src/transaction/log_append.cpp | 41 |
data_header_length | src/transaction/log_append.cpp | 1290 |
la_repl_filter | src/transaction/log_applier.c | 206 |
la_item | src/transaction/log_applier.c | 237 |
la_ha_apply_info | src/transaction/log_applier.c | 394 |
la_init_ha_apply_info | src/transaction/log_applier.c | 606 |
la_apply_statement_log | src/transaction/log_applier.c | 5496 |
la_apply_repl_log | src/transaction/log_applier.c | 5739 |
la_log_record_process | src/transaction/log_applier.c | 6101 |
la_log_commit | src/transaction/log_applier.c | 6531 |
la_init | src/transaction/log_applier.c | 6917 |
la_need_filter_out | src/transaction/log_applier.c | 7723 |
la_apply_log_file | src/transaction/log_applier.c | 8074 |
LA_RETRY_ON_ERROR | src/transaction/log_applier.h | 34 |
REPL_FILTER_TYPE | src/transaction/log_applier.h | 48 |
sl_write_catalog | src/transaction/log_applier_sql_log.c | 116 |
sl_read_catalog | src/transaction/log_applier_sql_log.c | 142 |
sl_write_sql | src/transaction/log_applier_sql_log.c | 519 |
sl_log_open | src/transaction/log_applier_sql_log.c | 570 |
MAKE_ZIP_LEN | src/transaction/log_compress.h | 33 |
log_zip | src/transaction/log_compress.h | 53 |
CDC_GET_TEMP_LOGPAGE | src/transaction/log_impl.h | 240 |
CDC_CHECK_TEMP_LOGPAGE | src/transaction/log_impl.h | 260 |
CDC_UPDATE_TEMP_LOGPAGE | src/transaction/log_impl.h | 271 |
MAX_CDC_LOGINFO_QUEUE_ENTRY | src/transaction/log_impl.h | 297 |
MAX_CDC_LOGINFO_QUEUE_SIZE | src/transaction/log_impl.h | 298 |
cdc_producer_state | src/transaction/log_impl.h | 787 |
CDC_PRODUCER_STATE | src/transaction/log_impl.h | 787 |
CDC_PRODUCER_STATE | src/transaction/log_impl.h | 792 |
cdc_consumer_request | src/transaction/log_impl.h | 794 |
CDC_CONSUMER_REQUEST | src/transaction/log_impl.h | 799 |
cdc_producer_request | src/transaction/log_impl.h | 801 |
CDC_PRODUCER_REQUEST | src/transaction/log_impl.h | 806 |
cdc_loginfo_entry | src/transaction/log_impl.h | 808 |
CDC_LOGINFO_ENTRY | src/transaction/log_impl.h | 813 |
cdc_temp_logbuf | src/transaction/log_impl.h | 815 |
CDC_TEMP_LOGBUF | src/transaction/log_impl.h | 815 |
CDC_TEMP_LOGBUF | src/transaction/log_impl.h | 819 |
cdc_producer | src/transaction/log_impl.h | 821 |
CDC_PRODUCER | src/transaction/log_impl.h | 821 |
temp_logbuf | src/transaction/log_impl.h | 842 |
CDC_PRODUCER | src/transaction/log_impl.h | 848 |
cdc_consumer | src/transaction/log_impl.h | 850 |
CDC_CONSUMER | src/transaction/log_impl.h | 850 |
CDC_CONSUMER | src/transaction/log_impl.h | 867 |
cdc_global | src/transaction/log_impl.h | 869 |
CDC_GLOBAL | src/transaction/log_impl.h | 869 |
loginfo_queue | src/transaction/log_impl.h | 877 |
CDC_GLOBAL | src/transaction/log_impl.h | 885 |
ovf_page_list | src/transaction/log_impl.h | 888 |
cdc_dataitem_type | src/transaction/log_impl.h | 896 |
CDC_DATAITEM_TYPE | src/transaction/log_impl.h | 902 |
cdc_dcl_type | src/transaction/log_impl.h | 904 |
CDC_DCL_TYPE | src/transaction/log_impl.h | 908 |
cdc_dml_type | src/transaction/log_impl.h | 910 |
CDC_DML_TYPE | src/transaction/log_impl.h | 910 |
CDC_DML_TYPE | src/transaction/log_impl.h | 918 |
CDC_IS_IGNORE_LOGINFO_ERROR | src/transaction/log_manager.c | 167 |
LOG_DUMMY_HA_SERVER_STATE | src/transaction/log_manager.c | 489 |
log_append_repl_info_and_commit_log | src/transaction/log_manager.c | 4647 |
log_append_commit_log | src/transaction/log_manager.c | 4779 |
log_append_supplemental_info | src/transaction/log_manager.c | 4837 |
log_append_supplemental_lsa | src/transaction/log_manager.c | 4892 |
log_append_supplemental_undo_record | src/transaction/log_manager.c | 4947 |
log_append_supplemental_serial | src/transaction/log_manager.c | 4972 |
cdc_log_extract | src/transaction/log_manager.c | 10658 |
cdc_loginfo_producer_execute | src/transaction/log_manager.c | 11064 |
cdc_check_log_page | src/transaction/log_manager.c | 11230 |
cdc_get_undo_record | src/transaction/log_manager.c | 11244 |
cdc_log_read_advance_and_preserve_if_needed | src/transaction/log_manager.c | 11306 |
cdc_get_recdes | src/transaction/log_manager.c | 11330 |
cdc_get_ovfdata_from_log | src/transaction/log_manager.c | 12184 |
cdc_get_overflow_recdes | src/transaction/log_manager.c | 12303 |
cdc_find_primary_key | src/transaction/log_manager.c | 12525 |
cdc_make_error_loginfo | src/transaction/log_manager.c | 12591 |
cdc_check_if_schema_changed | src/transaction/log_manager.c | 12688 |
cdc_get_attribute_size | src/transaction/log_manager.c | 12695 |
cdc_make_dml_loginfo | src/transaction/log_manager.c | 12818 |
cdc_make_ddl_loginfo | src/transaction/log_manager.c | 13277 |
cdc_make_dcl_loginfo | src/transaction/log_manager.c | 13381 |
cdc_make_timer_loginfo | src/transaction/log_manager.c | 13454 |
cdc_find_user | src/transaction/log_manager.c | 13533 |
cdc_compare_undoredo_dbvalue | src/transaction/log_manager.c | 13607 |
cdc_put_value_to_loginfo | src/transaction/log_manager.c | 13625 |
cdc_min_log_pageid_to_keep | src/transaction/log_manager.c | 14021 |
cdc_loginfo_producer_daemon_init | src/transaction/log_manager.c | 14030 |
cdc_daemons_init | src/transaction/log_manager.c | 14051 |
cdc_daemons_destroy | src/transaction/log_manager.c | 14066 |
cdc_pause_producer | src/transaction/log_manager.c | 14083 |
cdc_wakeup_producer | src/transaction/log_manager.c | 14098 |
cdc_kill_producer | src/transaction/log_manager.c | 14108 |
cdc_pause_consumer | src/transaction/log_manager.c | 14123 |
cdc_wakeup_consumer | src/transaction/log_manager.c | 14130 |
cdc_find_lsa | src/transaction/log_manager.c | 14137 |
cdc_check_lsa_range | src/transaction/log_manager.c | 14303 |
cdc_validate_lsa | src/transaction/log_manager.c | 14402 |
cdc_set_extraction_lsa | src/transaction/log_manager.c | 14465 |
cdc_reinitialize_queue | src/transaction/log_manager.c | 14479 |
cdc_get_start_point_from_file | src/transaction/log_manager.c | 14550 |
cdc_get_lsa_with_start_point | src/transaction/log_manager.c | 14726 |
cdc_make_loginfo | src/transaction/log_manager.c | 14835 |
cdc_initialize | src/transaction/log_manager.c | 14957 |
cdc_free_extraction_filter | src/transaction/log_manager.c | 14998 |
cdc_cleanup | src/transaction/log_manager.c | 15022 |
cdc_cleanup_consumer | src/transaction/log_manager.c | 15069 |
cdc_finalize | src/transaction/log_manager.c | 15087 |
cdc_set_configuration | src/transaction/log_manager.c | 15144 |
cdc_is_filtered_class | src/transaction/log_manager.c | 15165 |
cdc_is_filtered_user | src/transaction/log_manager.c | 15188 |
log_reader | src/transaction/log_reader.hpp | 36 |
fetch_mode | src/transaction/log_reader.hpp | 46 |
log_reader::set_lsa_and_fetch_page | src/transaction/log_reader.hpp | 162 |
log_reader::advance_when_does_not_fit | src/transaction/log_reader.hpp | 199 |
log_reader::skip | src/transaction/log_reader.hpp | 225 |
log_reader::fetch_page | src/transaction/log_reader.hpp | 269 |
LOG_READ_ALIGN | src/transaction/log_reader.hpp | 315 |
LOG_READ_ADD_ALIGN | src/transaction/log_reader.hpp | 331 |
LOG_READ_ADVANCE_WHEN_DOESNT_FIT | src/transaction/log_reader.hpp | 338 |
LOG_SUPPLEMENTAL_INFO | src/transaction/log_record.hpp | 136 |
LOG_RECORD_HEADER | src/transaction/log_record.hpp | 146 |
SUPPLEMENT_REC_TYPE | src/transaction/log_record.hpp | 418 |
LOG_REC_SUPPLEMENT | src/transaction/log_record.hpp | 434 |
log_rec_supplement | src/transaction/log_record.hpp | 435 |
LOG_REC_SUPPLEMENT | src/transaction/log_record.hpp | 435 |
LOG_GET_LOG_RECORD_HEADER | src/transaction/log_record.hpp | 441 |
cubrid-cdc.md— 상위 수준 동반 문서. 함께 참고할 문서:cubrid-log-manager-detail.md(CDC가 읽는 로그),cubrid-ha-replication.md(동일 로그를 소비하는 인접 컴포넌트).- 원시 분석 자료:
raw/code-analysis/cubrid/storage/cdc/하위 파일들. - 코드:
src/api/cubrid_log.c,src/transaction/log_reader.{cpp,hpp},log_applier_sql_log.{c,h},log_manager.c. - 방법론 참고:
knowledge/methodology/code-analysis-detail-doc.md.