CUBRID Heap Manager — 코드 수준 심층 분석
이 문서의 위치: 상위 분석 문서인
cubrid-heap-manager.md는 설계 의도와 이론적 배경을 다룬다. 이 문서는 모든 분기와 필드를 코드 수준에서 추적한다. 각 챕터는 독립적으로 읽을 수 있지만, 순서대로 읽으면 커널 내부에서 힙 레코드 하나가 겪는 전체 생애 주기를 따라갈 수 있다.
목차:
Chapter 1: 자료구조 지도
섹션 제목: “Chapter 1: 자료구조 지도”이 장은 나머지 문서 전체가 참조하는 필드 단위 레퍼런스다. MVCC 가시성, slotted-page 이론, forwarding/overflow 근거는 여기서 재도출하지 않는다 — 상위 문서(cubrid-heap-manager.md)를 참고하라. 세 계층은 레코드 OID로 연결된다: 페이지 계층(slotted_page.h), 힙 파일 계층(heap_file.c), 그리고 일시적인 연산/읽기 번들(heap_file.h).
1.1 관계 지도
섹션 제목: “1.1 관계 지도”graph TB
subgraph disk["디스크 상 힙 페이지"]
HDR["SPAGE_HEADER"]
SLOT0["SPAGE_SLOT[0]"]
SLOTN["SPAGE_SLOT[1..n]"]
HDR --> SLOT0
HDR --> SLOTN
end
STATS["HEAP_HDR_STATS<br/>헤더 페이지 슬롯 0"]
CHAIN["HEAP_CHAIN<br/>데이터 페이지 슬롯 0"]
HFID["HFID<br/>vfid + hpgid"] -->|hpgid| STATS
STATS -->|next_vpid| CHAIN
CHAIN -->|prev/next_vpid| CHAIN
subgraph mem["메모리 내 번들"]
OPCTX["HEAP_OPERATION_CONTEXT<br/>4x PGBUF_WATCHER"]
GETCTX["HEAP_GET_CONTEXT<br/>2x PGBUF_WATCHER"]
SCAN["HEAP_SCANCACHE<br/>스냅샷 / page_latch"]
NODE["HEAP_SCANCACHE_NODE"]
SCAN --> NODE
OPCTX -->|scan_cache_p| SCAN
GETCTX -->|scan_cache| SCAN
end
OPCTX -->|hfid| HFID
NODE -->|hfid| HFID
Figure 1-1. 디스크 구조(상단), 힙 파일 척추(HFID → 헤더 페이지 HEAP_HDR_STATS → HEAP_CHAIN 이중 연결 리스트), 그리고 하나의 연산 동안 해당 페이지에 latch를 잡는 메모리 내 번들.
불변 조건:
HEAP_HEADER_AND_CHAIN_SLOTID == 0. 모든 힙 페이지의 슬롯 0은 예약된 메타데이터다: 헤더 페이지에는HEAP_HDR_STATS, 나머지 모든 페이지에는HEAP_CHAIN이 들어간다. 이 조건은 두 곳에서 강제된다 — 슬롯 0은 페이지 초기화 시 이미 할당되어 있으므로 일반적인spage_insert가 이를 반환하지 않으며,HEAP_ISJUNK_OID는slotid == 0인 OID를 모두 거부한다. 슬롯 0에 사용자 레코드가 들어가면 체인 탐색이 레코드 바이트를 chain/stats 구조체로 읽어 힙을 손상시킨다.
// HEAP_HEADER_AND_CHAIN_SLOTID -- src/storage/heap_file.h#define HEAP_HEADER_AND_CHAIN_SLOTID 0 /* Slot for chain and header */
// HEAP_ISJUNK_OID -- src/storage/heap_file.h#define HEAP_ISJUNK_OID(oid) \ ((oid)->slotid == HEAP_HEADER_AND_CHAIN_SLOTID \ || (oid)->slotid < 0 || (oid)->volid < 0 || (oid)->pageid < 0)1.2 페이지 계층 — SPAGE_HEADER와 SPAGE_SLOT
섹션 제목: “1.2 페이지 계층 — SPAGE_HEADER와 SPAGE_SLOT”고정 헤더, 앞쪽에서 자라는 슬롯 배열, 뒤쪽에서 자라는 레코드. 세부 메커니즘은 Chapter 2에서 다룬다.
// spage_header -- src/storage/slotted_page.h (comments in table)struct spage_header{ PGNSLOTS num_slots, num_records; INT16 anchor_type; /* ANCHORED / ANCHORED_DONT_REUSE_SLOTS / UNANCHORED_* */ unsigned short alignment; /* char, short, int, double */ int total_free, cont_free, offset_to_free_area; int reserved1; int flags; /* always SPAGE_HEADER_FLAG_NONE */ unsigned int is_saving:1; /* save-for-undo */ unsigned int need_update_best_hint:1; /* best-hint refresh */ unsigned int reserved_bits:30;};| 필드 | 역할 / 존재 이유 |
|---|---|
num_slots, num_records | 슬롯 배열의 길이와 활성 레코드 수. 삭제 후에도 슬롯은 남으므로 num_slots >= num_records. |
anchor_type | ANCHORED/ANCHORED_DONT_REUSE_SLOTS/UNANCHORED_*. 힙은 anchored 방식을 사용하므로 슬롯 id가 안정적으로 유지되어 OID가 이동하지 않는다. |
alignment | 바이트 단위 레코드 정렬 (힙은 INT_ALIGNMENT 사용). |
total_free | 전체 여유 바이트. 컴팩션 후 레코드+슬롯 크기를 수용할 수 있는지 검사할 때 사용. |
cont_free | 단일 연속 공백의 바이트 수. 이 값이 부족하지만 total_free가 충분하면 먼저 컴팩션을 수행한다. |
offset_to_free_area | 새 레코드를 기록하는 bump pointer 위치. |
reserved1 | 예약 int; 헤더를 8바이트 정렬로 유지. |
flags | 페이지 플래그; SPAGE_HEADER_FLAG_NONE만 사용. |
is_saving | 1비트: undo를 위한 공간 절약 (Ch 10). |
need_update_best_hint | 1비트: best hint가 오래됐으니 estimates를 갱신하라는 신호 (Ch 9). |
reserved_bits | 30비트 패딩; bitfield 워드 레이아웃을 고정. |
// spage_slot -- src/storage/slotted_page.h (4-byte disk slot)struct spage_slot{ unsigned int offset_to_record:14; /* Byte offset to start of record */ unsigned int record_length:14; /* Length of record */ unsigned int record_type:4; /* REC_HOME, REC_NEWHOME, ... */};| 필드 | 역할 / 존재 이유 |
|---|---|
offset_to_record (14b) | 레코드 바이트의 오프셋. 슬롯 id를 안정적으로 유지하는 간접 참조 역할을 하며, 컴팩션 시 이 값만 재기록한다. 최대값 16383. |
record_length (14b) | 레코드 바이트 길이. 복사/peek의 범위를 한정한다. 페이지 내 레코드의 상한이 16383바이트이며, 초과 시 overflow로 이어진다. |
record_type (4b) | REC_HOME/NEWHOME/RELOCATION/BIGONE/DELETED_WILL_REUSE/MARKDELETED/ASSIGN_ADDRESS/… — 모든 흐름에서 분기의 기준 키 (Ch 3). |
슬롯 전체가 정확히 4바이트 (14+14+4 = 32비트)다.
1.3 힙 파일 계층 — HFID, HEAP_HDR_STATS, HEAP_CHAIN
섹션 제목: “1.3 힙 파일 계층 — HFID, HEAP_HDR_STATS, HEAP_CHAIN”HFID는 힙의 이름이다: 파일 id와 헤더 페이지 id의 조합.
// hfid -- src/storage/storage_common.hstruct hfid{ VFID vfid; /* Volume and file identifier */ INT32 hpgid; /* First page identifier (the header page) */};| 필드 | 역할 / 존재 이유 |
|---|---|
vfid | 힙 파일의 (volid, fileid). 모든 페이지 할당/해제에 사용된다. |
hpgid | 헤더 페이지의 page id — 힙의 진입점. vfid.volid와 결합하면 next_vpid 탐색을 시작하는 헤더 VPID가 된다. |
HEAP_HDR_STATS는 힙의 전역 제어 블록으로, 헤더 페이지의 슬롯 0에 저장된다. 내부의 estimates 블록은 best-space hint 캐시(Chapter 9)이며, 로그를 남기지 않는다.
// heap_hdr_stats -- src/storage/heap_file.c (comments elided; see table)struct heap_hdr_stats{ OID class_oid; /* the first field MUST be class_oid */ VFID ovf_vfid; VPID next_vpid; /* the 2nd page of the heap file */ int unfill_space; struct { int num_pages, num_recs; float recs_sumlen; int num_other_high_best, num_high_best, num_substitutions; int num_second_best, head_second_best, tail_second_best, head; VPID last_vpid; /* todo: move out of estimates */ VPID full_search_vpid; VPID second_best[HEAP_NUM_BEST_SPACESTATS]; /* 10 hints */ HEAP_BESTSPACE best[HEAP_NUM_BEST_SPACESTATS]; /* 10 hints */ } estimates; int reserve0_for_future, reserve1_for_future, reserve2_for_future;};| 필드 | 역할 / 존재 이유 |
|---|---|
class_oid | 저장된 클래스의 OID. 반드시 첫 번째 필드여야 한다 — 슬롯 0의 선두 OID가 class id이며, 유효성 검사를 위해 HEAP_CHAIN과 공유하는 규약이다. |
ovf_vfid | overflow 파일 id (REC_BIGONE 본체를 저장). 첫 overflow 전까지는 null. |
next_vpid | 두 번째 페이지의 VPID — 페이지 체인의 head. |
unfill_space | 삽입의 여유 공간 하한선; 제자리 갱신이 가능하도록 여유분을 남긴다. |
estimates.num_pages, num_recs, recs_sumlen | 예상 페이지/레코드 수와 전체 레코드 바이트 합계. recs_sumlen으로 평균 길이를 계산한다. |
estimates.num_other_high_best | best[]에 없는 양질의 페이지 수. 파일을 확장하기 전에 더 넓은 탐색을 유도한다. |
estimates.num_high_best | best[] 항목 중 HEAP_DROP_FREE_SPACE 이상인 수. 0이 되면 재스캔한다. |
estimates.num_substitutions | 교체 횟수. second-best 승격 판단에 활용된다. |
estimates.num_second_best, head_second_best, tail_second_best | second_best[] 링의 크기, 읽기 인덱스(가장 오래된 항목), 쓰기 인덱스. |
estimates.head | best[] 링의 head 인덱스. 할당/스캔의 시작점. |
estimates.last_vpid | 마지막/추가 페이지. estimates 외부로 이전하라는 todo 주석이 붙어 있다. |
estimates.full_search_vpid | 호출 간 점진적 스캔의 재개 지점. |
estimates.second_best[10] | 여유 공간이 적당한 VPID 10개의 링. best[]가 소진됐을 때 사용한다. |
estimates.best[10] | HEAP_BESTSPACE 10개의 링. 1차 hint 세트. HEAP_NUM_BEST_SPACESTATS == 10. |
reserve0/1/2_for_future | 예약 int. |
불변 조건:
estimates는 hint일 뿐, 진실이 아니다. 변경 사항은 로그를 남기지 않는다 — “힌트 용도로만 쓰인다”, “정확하지 않을 수 있다”, “중복 페이지가 포함될 수 있다”는 주석이 붙어 있다. 사용 전에 후보 페이지의 실제total_free를 재검증해야 한다.best[]를 맹목적으로 신뢰하면 이미 꽉 찬 페이지를 대상으로 삼을 수 있다 (Chapter 9).
HEAP_CHAIN은 모든 비헤더 페이지의 슬롯 0에 위치하며, 이중 연결 리스트를 구성한다.
// heap_chain -- src/storage/heap_file.c (Double-linked)struct heap_chain{ OID class_oid; /* the first must be class_oid */ VPID prev_vpid; /* Previous page */ VPID next_vpid; /* Next page */ MVCCID max_mvccid; /* Max MVCCID of any MVCC operation on this page */ INT32 flags; /* 2 high bits encode vacuum state */};| 필드 | 역할 / 존재 이유 |
|---|---|
class_oid | 클래스 OID (선두 필드 규약 동일). 헤더 페이지와 데이터 페이지를 구분하지 않고 페이지 유효성을 검사할 수 있다. |
prev_vpid | 이전 페이지 VPID. 역방향 탐색 및 체인 복구에 사용. |
next_vpid | 다음 페이지 VPID. 전방 탐색 — HEAP_HDR_STATS.next_vpid가 공급하는 척추. |
max_mvccid | 이 페이지에서 수행된 임의 연산 중 가장 큰 MVCCID. Vacuum 예측기: vacuum의 oldest MVCCID보다 앞서면 페이지가 완전히 vacuum됐다고 판단한다. MVCCID_NULL로 초기화 (Ch 8). |
flags | INT32. 상위 2비트 = vacuum 상태 (HEAP_PAGE_VACUUM_NONE/ONCE/UNKNOWN, 마스크 0xC0000000). 나머지는 예약. |
Vacuum 상태는 flags의 상위 2비트에 압축되며, HEAP_PAGE_SET_VACUUM_STATUS / HEAP_PAGE_GET_VACUUM_STATUS로만 접근한다:
// HEAP_PAGE_FLAG_VACUUM_STATUS_MASK -- src/storage/heap_file.c#define HEAP_PAGE_FLAG_VACUUM_STATUS_MASK 0xC0000000#define HEAP_PAGE_FLAG_VACUUM_ONCE 0x80000000#define HEAP_PAGE_FLAG_VACUUM_UNKNOWN 0x40000000// status == NONE => both bits clear /* <- the HEAP_PAGE_VACUUM_NONE encoding */불변 조건:
max_mvccid는 페이지 단위로 단조 증가한다. 모든 MVCC 연산은if (MVCC_ID_PRECEDES (chain->max_mvccid, mvccid)) chain->max_mvccid = mvccid;를 수행한다(디버그 단언 포함). 이 값이 역행하면 아직 대기 중인 버전을 가진 페이지를 vacuum이 해제할 수 있다.
1.4 연산 번들 — HEAP_OPERATION_CONTEXT와 관련 열거형
섹션 제목: “1.4 연산 번들 — HEAP_OPERATION_CONTEXT와 관련 열거형”모든 쓰기 연산에 인자로 전달되는 단일 구조체: type과 update_in_place가 분기를 결정하고, 최대 네 개의 watcher, 홈 레코드 스택 버퍼, 출력 OID가 함께 실린다.
// HEAP_OPERATION_TYPE -- src/storage/heap_file.htypedef enum{ HEAP_OPERATION_NONE = 0, HEAP_OPERATION_INSERT, HEAP_OPERATION_DELETE, HEAP_OPERATION_UPDATE} HEAP_OPERATION_TYPE;
// update_inplace_style -- src/storage/heap_file.henum update_inplace_style{ UPDATE_INPLACE_NONE = 0, /* None */ UPDATE_INPLACE_CURRENT_MVCCID = 1, /* non-MVCC in-place update with current MVCC ID */ UPDATE_INPLACE_OLD_MVCCID = 2 /* non-MVCC in-place update, preserves old MVCC ID */};typedef enum update_inplace_style UPDATE_INPLACE_STYLE;#define HEAP_IS_UPDATE_INPLACE(update_inplace_style) \ ((update_inplace_style) != UPDATE_INPLACE_NONE)HEAP_OPERATION_TYPE은 쓰기 경로를 태그한다. UPDATE_INPLACE_STYLE은 직교하는 개념이다: MVCC 갱신은 물리적으로 제자리에서 수행되지만 UPDATE_INPLACE_NONE에 해당한다(소스: “mvcc update is also executed inplace, but coresponds to UPDATE_INPLACE_NONE”) — 따라서 UPDATE_INPLACE_NONE은 새로운 논리 버전 생성을 의미하고, HEAP_IS_UPDATE_INPLACE(스타일 1/2)는 진정한 비MVCC 덮어쓰기다. Chapter 6에서 분기 흐름을 상세히 다룬다.
// heap_operation_context -- src/storage/heap_file.h (condensed)struct heap_operation_context{ HEAP_OPERATION_TYPE type; UPDATE_INPLACE_STYLE update_in_place; HFID hfid; OID oid; OID class_oid; RECDES *recdes_p; HEAP_SCANCACHE *scan_cache_p; RECDES map_recdes; OID ovf_oid; /* overflow transient */ RECDES home_recdes; char home_recdes_buffer[IO_MAX_PAGE_SIZE + MAX_ALIGNMENT]; INT16 record_type; FILE_TYPE file_type; PGBUF_WATCHER home_page_watcher, overflow_page_watcher, header_page_watcher, forward_page_watcher; PGBUF_WATCHER *home_page_watcher_p, *overflow_page_watcher_p, *header_page_watcher_p, *forward_page_watcher_p; /* the handles */ OID res_oid; bool is_logical_old; /* logical output */ bool is_redistribute_insert_with_delid; bool is_bulk_op; bool use_bulk_logging; bool do_supplemental_log; LOG_LSA supp_undo_lsa, supp_redo_lsa; PERF_UTIME_TRACKER *time_track; /* perf stat dump */};| 필드 | 역할 / 존재 이유 |
|---|---|
type | 쓰기 연산 종류 (INSERT/DELETE/UPDATE/NONE). 분기 결정. |
update_in_place | 갱신 스타일. 제자리 변경 vs. 새 버전 재배치 (Ch 6). |
hfid | 대상 힙. 파일/헤더 페이지를 특정해 할당과 best-space 조회에 사용. |
oid | 입력 OID (삭제/갱신 대상. 삽입 시 무시됨) — 홈 주소. |
class_oid | 클래스. 잠금, MVCC 헤더 구성, 인덱스 유지에 활용. |
recdes_p | 호출자의 레코드 디스크립터 — 새 바이트. |
scan_cache_p | latch된 페이지와 스냅샷 재사용 (선택). |
map_recdes | overflow 삽입 중 구성된 map 레코드 (overflow 객체를 가리킴). |
ovf_oid | overflow 객체 위치. REC_BIGONE 시 설정. |
home_recdes | 변경 전에 읽은 홈 레코드 디스크립터. |
home_recdes_buffer | home_recdes를 백업하는 인라인 스택 버퍼. 힙 할당을 회피한다. |
record_type | 변경 전 원본 레코드의 타입. |
file_type | FILE_HEAP/FILE_HEAP_REUSE_SLOTS. 슬롯 재사용 결정 (Ch 7). |
home/overflow/header/forward_page_watcher | 내장된 PGBUF_WATCHER 저장 슬롯 네 개 (overflow → REC_BIGONE, header → best-space 갱신, forward → REC_RELOCATION/REC_NEWHOME). |
*_watcher_p (×4) | 네 watcher에 대한 포인터 — 실제 핸들. null이면 해당 페이지 미관여. |
res_oid | 출력 OID. 삽입 시 할당된 주소. |
is_logical_old | 출력: 초기 레코드가 REC_ASSIGN_ADDRESS가 아니었음. 로깅에 사용. |
is_redistribute_insert_with_delid | 유효한 delid를 가진 파티션 재분배로부터 온 삽입. |
is_bulk_op, use_bulk_logging, do_supplemental_log, supp_undo_lsa, supp_redo_lsa | 로깅/벌크 제어: 벌크 삽입 플래그(MVCC 연산도 비활성화), 벌크 로그 경로, 보조 로그 활성화, 보조 undo/redo 이미지 LSA. |
time_track | PERF_UTIME_TRACKER * — 성능 통계 덤프. |
불변 조건: 페이지는 반드시
_pwatcher 포인터로 접근한다. 내장된 네 개의PGBUF_WATCHER는 저장 공간이고,*_watcher_p포인터가 핸들이다(“직접 참조해서는 안 된다”). 코드는 latch할 페이지를 가리키도록 내장 watcher 하나를 설정하고, 나머지는 NULL로 둔다. 정리 시 NULL이 아닌 것만 unfix한다. 내장 watcher를 직접 건드리면 이중 unfix 또는 latch 누수가 발생한다.
1.5 읽기 번들 — HEAP_GET_CONTEXT
섹션 제목: “1.5 읽기 번들 — HEAP_GET_CONTEXT”읽기 측 대응 구조체. 쓰기와 달리 헤더 페이지나 overflow 페이지를 건드릴 일이 없으므로 watcher가 두 개 — 홈과 forward — 만 필요하다.
// heap_get_context -- src/storage/heap_file.h (comments in table)struct heap_get_context{ INT16 record_type; const OID *oid_p; OID forward_oid; /* of REC_RELOCATION or REC_BIGONE */ OID *class_oid_p; RECDES *recdes_p; HEAP_SCANCACHE *scan_cache; PGBUF_WATCHER home_page_watcher, fwd_page_watcher; bool ispeeking; /* PEEK or COPY */ int old_chn; PGBUF_LATCH_MODE latch_mode;};| 필드 | 역할 / 존재 이유 |
|---|---|
record_type | oid_p 위치의 타입. forwarding 분기 키 (Ch 5): REC_HOME이면 제자리, REC_RELOCATION/REC_BIGONE이면 forward_oid를 추적. |
oid_p | 요청된 OID (입력, const). 읽기의 시작점. |
forward_oid | 홈 슬롯이 가리키는 OID. 두 번째 hop. record_type이 요구할 때 채워진다. |
class_oid_p | 클래스 OID (입/출력). 스냅샷/CHN에 필요. |
recdes_p | 바이트가 반환되는 위치. 페이지 내(PEEK) 또는 복사 버퍼(COPY). |
scan_cache | 지배 HEAP_SCANCACHE. mvcc_snapshot을 공급. |
home_page_watcher, fwd_page_watcher | 홈 및 forward 페이지 watcher. forward는 두 번째 hop에서 latch되며, forwarding 전까지는 null. |
ispeeking | PEEK(zero-copy, latch 유지) vs COPY(latch를 더 빨리 해제). |
old_chn | 호출자의 캐시된 CHN. 레코드 CHN과 일치하면 복사를 건너뜀 (상위 문서 참고). |
latch_mode | 기본적으로 READ. serial increment 등에서는 WRITE. |
1.6 스캔 번들 — HEAP_SCANCACHE와 HEAP_SCANCACHE_NODE
섹션 제목: “1.6 스캔 번들 — HEAP_SCANCACHE와 HEAP_SCANCACHE_NODE”가장 오래 살아 있는 번들: 여러 번의 get/next 호출에 걸쳐 고정된 페이지, 스냅샷, latch/lock 정책을 캐시한다. 현재 힙을 위한 HEAP_SCANCACHE_NODE 하나를 내장하고, 파티션 스캔을 위한 추가 노드 리스트도 가진다.
// heap_scancache_node -- src/storage/heap_file.hstruct heap_scancache_node{ HFID hfid; /* Heap file of scan */ OID class_oid; /* Class oid of scanned instances */ const char *classname;};
// heap_scancache -- src/storage/heap_file.h (condensed C++ class; comments in table)struct heap_scancache{ int debug_initpattern; HEAP_SCANCACHE_NODE node; LOCK page_latch; /* NULL_LOCK to skip per-page lock */ bool cache_last_fix_page, mvcc_disabled_class; PGBUF_WATCHER page_watcher; int num_btids; multi_index_unique_stats *m_index_stats; FILE_TYPE file_type; MVCC_SNAPSHOT *mvcc_snapshot; HEAP_SCANCACHE_NODE_LIST *partition_list;private: cubmem::single_block_allocator *m_area; /* the one private member */};| 필드 | 역할 / 존재 이유 |
|---|---|
node.hfid | 스캔 대상 힙 — 스캔이 탐색하는 파일. |
node.class_oid | 스캔하는 인스턴스의 클래스. 잠금 및 가시성 판단에 사용. |
node.classname | 캐시된 클래스 이름. 로깅/진단 용도로, 레코드마다 재조회를 피한다. |
debug_initpattern | 초기화 sentinel. 디버그 빌드에서 미초기화 scancache 사용을 감지한다. |
node | 현재 HEAP_SCANCACHE_NODE. HEAP_SCANCACHE_SET_NODE로 설정. |
page_latch | 힙 페이지에 대한 LOCK. 클래스가 이미 S/SIX/X로 잠겨 있으면 NULL_LOCK. |
cache_last_fix_page | 마지막 fix 페이지와 area 메모리를 유지. 한 페이지에 레코드가 많을 때 재fix를 피한다. |
mvcc_disabled_class | 비MVCC 클래스 여부. 카탈로그/비MVCC 클래스에 대한 스냅샷 가시성 검사를 건너뜀. |
page_watcher | 캐시된 fix 페이지를 잡는 watcher — cache_last_fix_page의 핸들. |
num_btids | 클래스의 인덱스 수. 스캔 기반 갱신의 인덱스 유지 작업 범위를 결정. |
m_index_stats | 인덱스별 유니크 통계 (소스 주석은 이 위치가 적절한지 의문을 제기함). |
file_type | FILE_HEAP/FILE_HEAP_REUSE_SLOTS. 슬롯 재사용 결정이 스캔에서도 사용 가능. |
mvcc_snapshot | 지배 MVCC 스냅샷. 가시성의 단일 소스로, get context에 전달된다. |
partition_list | 서브 힙을 위한 HEAP_SCANCACHE_NODE 리스트. 파티션 스캔은 여러 힙을 가로지른다. |
(private) m_area | COPY-area 메서드를 백업하는 cubmem::single_block_allocator *. |
불변 조건: 페이지 잠금을 커버하는 클래스 잠금이 없다면
page_latch는 반드시 non-NULL이어야 한다. “힙 페이지에 대한 잠금을 안전하게 생략할 수 있을 때, 즉 클래스가 S/SIX/X로 유지될 때NULL_LOCK이 될 수 있다”고 명시되어 있다. 해당 커버 잠금 없이NULL_LOCK을 사용하면 두 트랜잭션이 직렬화 없이 동일 페이지를 건드릴 수 있다.
1.7 HEAP_BESTSPACE
섹션 제목: “1.7 HEAP_BESTSPACE”best-space 기계(Chapter 9)가 거래하는 단위. estimates.best[]의 각 항목이 이 구조체 하나이며, 전역 best-space 해시도 이를 캐시한다.
// heap_bestspace -- src/storage/heap_file.hstruct heap_bestspace{ VPID vpid; /* Vpid of one of the best pages */ int freespace; /* Estimated free space in this page */};| 필드 | 역할 / 존재 이유 |
|---|---|
vpid | 여유 공간이 충분한 페이지의 (volid, pageid). 삽입이 먼저 시도하는 페이지. |
freespace | 추정 여유 바이트. 후보를 순위 매기는 데 쓰이며, 실제 사용 전에 SPAGE_HEADER.total_free와 재검증한다 (§1.3). |
1.8 챕터 요약 — 핵심 정리
섹션 제목: “1.8 챕터 요약 — 핵심 정리”- 세 계층, 하나의 OID 실. slotted page가 바이트를 저장하고, 힙 파일 계층(
HFID→HEAP_HDR_STATS→ 연결된HEAP_CHAIN)이 정체성과 순서를 부여하며, 번들이 latch와 정책을 하나의 호출 동안 운반한다. - 슬롯 0은 신성하다.
HEAP_HEADER_AND_CHAIN_SLOTID == 0이HEAP_HDR_STATS(헤더)와HEAP_CHAIN(데이터)을 위해 예약하며,HEAP_ISJUNK_OID가 사용자 OID를 차단한다. SPAGE_SLOT은 4바이트 14/14/4 비트필드다.offset_to_record는 안정적인 주소 간접 참조이고,record_length는 범위를 한정하며, 4비트record_type이 모든 흐름의 분기 키다.HEAP_HDR_STATS.estimates와HEAP_BESTSPACE는 조언적이며 로그를 남기지 않는다. best/second-best 링(각 10개)은 hint로, 실제 여유 공간과 재검증한다.HEAP_CHAIN은flags에 vacuum 상태를 압축한다. 상위 두 비트가VACUUM_NONE/ONCE/UNKNOWN을 인코딩하고,max_mvccid(단조 증가)가 페이지의 청결 여부를 예측한다.HEAP_OPERATION_CONTEXT는 간접 참조로 네 개의 watcher를 소유한다.*_watcher_p포인터만이 유효한 핸들이며,type+update_in_place가 경로를 선택한다.HEAP_GET_CONTEXT는 더 가벼운 읽기 쌍둥이(홈 + forward만)이고,HEAP_SCANCACHE(HEAP_SCANCACHE_NODE포함)는 여러 호출에 걸쳐 스냅샷, 캐시된 페이지,page_latch정책을 유지한다.
Chapter 2: 슬롯 페이지 기본 요소와 페이지 초기화
섹션 제목: “Chapter 2: 슬롯 페이지 기본 요소와 페이지 초기화”모든 힙 레코드 — REC_HOME, REC_NEWHOME, REC_BIGONE, REC_RELOCATION — 는 슬롯 페이지(slotted page) 안에 저장된다. 힙 레이어는 페이로드를 직접 다루지 않고, slotted_page.c에 가변 길이 영역의 할당·크기 조정·반환을 위임한 뒤 안정적인 슬롯 ID를 돌려받는다. 이 챕터에서는 그 기반 구조를 해부한다: 레이아웃, 여유 공간 불변식, 네 가지 앵커 타입, 그리고 4~8장이 호출하는 슬롯 기본 연산들. 상위 문서(cubrid-heap-manager.md)는 CUBRID가 안정적인 OID와 움직이는 바이트 오프셋을 분리하는 이유를 다룬다. 여기서는 오프셋이 바뀌는 동안에도 슬롯 ID가 어떻게 고정되는지 그 방법을 추적한다.
2.1 메모리 안의 페이지
섹션 제목: “2.1 메모리 안의 페이지”슬롯 페이지는 고정 버퍼(SPAGE_DB_PAGESIZE)이며, 오프셋 0에 SPAGE_HEADER가 위치한다. 레코드 페이로드는 헤더 바로 다음부터 아래쪽으로 자라고, 슬롯 배열은 페이지 끝에서 위쪽으로 자란다. 두 영역 사이의 간격이 연속 여유 공간이다. SPAGE_SLOT은 4바이트짜리 간접 참조 단위로, 세 개의 비트 필드가 압축되어 있다:
// spage_slot -- src/storage/slotted_page.hstruct spage_slot{ unsigned int offset_to_record:14; /* byte offset of record start */ unsigned int record_length:14; /* current record length */ unsigned int record_type:4; /* REC_HOME, REC_NEWHOME, ... */};14비트 필드는 페이지 크기를 최대 16 KB로 제한하며, 이는 CUBRID의 최대 페이지 크기에 해당한다. record_type은 3장에서 해석 방식을 결정할 때 읽는 값이다. 슬롯 N은 마지막 4바이트(spage_find_slot: slot_p = page_p + SPAGE_DB_PAGESIZE - sizeof(SPAGE_SLOT); slot_p -= slot_id;)에서 역순으로 세어 찾는다. 이 기하 구조 덕분에 슬롯 배열(위쪽으로 증가, slotN..slot0)과 레코드 영역(아래쪽으로 증가)이 여유 간격에서 만나게 되며, 그 간격의 첫 번째 바이트가 offset_to_free_area이다.
SPAGE_HEADER는 이 챕터가 조작하는 관리 정보를 담는다: num_slots(배열 길이 / 반복 한계)와 num_records(살아 있는 레코드 수, 따라서 num_slots - num_records가 재사용 가능한 슬롯 풀); anchor_type(§2.3)과 alignment; 아래에서 설명할 세 가지 여유 공간 카운터; 그리고 §2.7의 undo 예약을 활성화하는 is_saving.
불변식 (헤더 일관성). 모든 기본 연산의 진입·퇴출 시점마다:
total_free >= 0,0 <= cont_free <= total_free,offset_to_free_area < SPAGE_DB_PAGESIZE이고alignment정렬됨,0 <= num_records <= num_slots.spage_verify_header/SPAGE_VERIFY_HEADER가 이를 강제한다. 위반 시 여유 공간 산술이 깨지며, 이후 삽입이 살아 있는 레코드를 덮어쓸 수 있다.
2.2 페이지 초기화: spage_initialize와 spage_verify_header
섹션 제목: “2.2 페이지 초기화: spage_initialize와 spage_verify_header”spage_initialize는 분기 없이 처음 불변식을 확립하는 유일한 함수다(디버그 전용 assert (spage_is_valid_anchor_type (slot_type))이 앵커 범위 1..4를 확인한다). num_slots/num_records를 0으로 초기화하고, is_saving과 anchor_type을 저장한 뒤, total_free = DB_ALIGN (SPAGE_DB_PAGESIZE - sizeof (SPAGE_HEADER), alignment), cont_free = total_free, offset_to_free_area = DB_ALIGN (sizeof (SPAGE_HEADER), alignment)로 설정한다. 표준 빈 페이지 상태: cont_free == total_free, 두 카운터 모두 0, 커서는 정렬된 헤더 바로 다음. total_free는 헤더를 제외하지만, 슬롯 배열 공간은 아직 차감하지 않는다 — 슬롯 바이트는 할당 시점마다 지연 청구된다.
spage_verify_header는 §2.1의 런타임 감사자다: 모든 경계 조건을 AND로 검사하고, 실패 시 헤더를 포맷팅한 뒤 ER_SP_INVALID_HEADER를 발생시키고 assert (false)를 호출한다. SPAGE_VERIFY_HEADER는 그 매크로 형태로, 거의 모든 기본 연산 경계에서 쓰인다.
2.3 네 가지 앵커 타입과 슬롯 재사용
섹션 제목: “2.3 네 가지 앵커 타입과 슬롯 재사용”| 앵커 타입 | 삭제 시 슬롯 ID는… | 재사용 |
|---|---|---|
ANCHORED (1) | 유지됨; REC_DELETED_WILL_REUSE로 표시 | 이후 삽입이 같은 ID를 재사용 가능 |
ANCHORED_DONT_REUSE_SLOTS (2) | 유지됨; REC_MARKDELETED로 표시 | spage_reclaim 전까지 재사용 불가; OID가 묵시적으로 재할당되지 않도록 힙이 선택 |
UNANCHORED_ANY_SEQUENCE (3) | 제거됨; 마지막 슬롯이 빈 자리로 이동 | ID 불안정, 순서 보장 없음 |
UNANCHORED_KEEP_SEQUENCE (4) | 제거됨; 상위 슬롯들 memmove 이동 | ID 불안정, 순서 보장 |
힙 데이터 페이지는 ANCHORED_DONT_REUSE_SLOTS를 사용한다: OID가 (volid, pageid, slotid)이므로, slotid는 레코드보다 오래 살아 있어야 한다. spage_is_valid_anchor_type은 1..4 범위 밖의 값을 거부한다.
2.4 여유 공간 검사: spage_has_enough_total_space, spage_has_enough_contiguous_space, spage_check_space
섹션 제목: “2.4 여유 공간 검사: spage_has_enough_total_space, spage_has_enough_contiguous_space, spage_check_space”spage_has_enough_total_space는 “공간이 있는가”를 답한다: space <= 0이면 true, 그 외에는 space <= total_free이되 is_saving 페이지에서는 spage_get_total_saved_spaces(...)를 차감한다(undo 예약분). spage_has_enough_contiguous_space는 space <= cont_free || spage_compact(...) == NO_ERROR — 부족하면 압축(compaction)을 부작용으로 실행한다. spage_check_space는 두 검사를 조합한다: 전체 공간 부족 → SP_DOESNT_FIT; 연속 공간 부족(압축 오류) → SP_ERROR; 그 외 → SP_SUCCESS.
리턴 코드 계약.
SP_SUCCESS (1)= 완료;SP_DOESNT_FIT (3)= 압축 후에도 너무 꽉 차 있으므로 호출자가 다른 페이지를 찾아야 함;SP_ERROR (-1)= 하드 내부 실패(압축 오류, 손상된 슬롯, 앵커 위반), “다른 곳을 시도하라”는 의미가 아님. 혼동하면 정확성 버그가 된다:SP_DOESNT_FIT을SP_ERROR로 잘못 처리하면 페이지를 옮겼어야 할 트랜잭션이 중단된다.
2.5 슬롯 할당: spage_find_free_slot과 spage_find_empty_slot
섹션 제목: “2.5 슬롯 할당: spage_find_free_slot과 spage_find_empty_slot”spage_find_free_slot은 ID를 선택한다: 모든 슬롯이 살아 있으면(num_slots == num_records) 구멍이 없으므로 slot_id = num_slots(추가); 그 외에는 start_slot부터 순방향으로 record_type == REC_DELETED_WILL_REUSE인 첫 번째 슬롯을 찾아 재사용한다. REC_DELETED_WILL_REUSE만 재사용 가능하다 — REC_MARKDELETED(ANCHORED_DONT_REUSE_SLOTS 툼스톤)는 의도적으로 매칭하지 않으므로, 힙 OID는 vacuum이 회수하기 전까지 재할당되지 않는다.
spage_find_empty_slot은 할당-예약 기본 연산으로, 분기별로 진행된다: (1) spage_has_enough_total_space 실패 → SP_DOESNT_FIT; (2) spage_find_free_slot이 SP_ERROR를 반환하거나 num_slots를 초과하는 ID → SP_ERROR; (3) 새 ID(slot_id == num_slots)인 경우 space에 sizeof(SPAGE_SLOT)을 더하고 spage_check_space를 실행(배열이 간격을 소비), SP_SUCCESS가 아니면 그 상태 반환, 성공 시 num_slots++; (4) 재사용 구멍은 이미 전체 공간을 검증했으므로 cont_free만 재확인(실패 → SP_ERROR); (5) 두 경로 모두 예약 블록에서 수렴 — 슬롯을 설정하고, num_records++, 두 카운터에서 space를 차감하고, offset_to_free_area를 전진 → SP_SUCCESS.
2.6 삽입, 제자리 갱신, 그리고 2단계 갱신 경로
섹션 제목: “2.6 삽입, 제자리 갱신, 그리고 2단계 갱신 경로”spage_insert는 spage_find_slot_for_insert(...)를 호출하고, SP_SUCCESS 시 spage_insert_data(...)를 호출하는 방식으로 구성된다. spage_find_slot_for_insert는 spage_check_record_for_insert를 호출한다 — 초과 크기는 SP_DOESNT_FIT으로 거부하고, REC_MARKDELETED/REC_DELETED_WILL_REUSE 디스크립터 타입은 REC_HOME으로 재기록한다(툼스톤 삽입 불가). spage_insert_data는 REC_ASSIGN_ADDRESS(TRANID 플레이스홀더)와 일반 memcpy로 분기하며, 각각 오버플로우를 검사한다.
spage_insert_at은 UNANCHORED 페이지와 복구를 위한 명시적 ID 변형이다: slot_id <= num_slots 검증(SP_ERROR/ER_SP_UNKNOWN_SLOTID), 이후 spage_find_empty_slot_at을 거쳐 spage_add_new_slot(추가) 또는 spage_take_slot_in_use(재사용, ANCHORED* 페이지에서 사용 중인 슬롯을 재대상으로 지정하면 ER_SP_BAD_INSERTION_SLOT, 그 외엔 배열을 위로 밀어올림)로 분기한다.
갱신. spage_update는 total_free_save를 저장하고, 공간 여부를 확인(spage_check_updatable → 여유 없으면 SP_DOESNT_FIT)한 뒤 크기에 따라 분기한다: length <= slot_p->record_length이면 spage_update_record_in_place(빠른 경로), 그 외에는 spage_update_record_after_compact(증가). is_saving이면 순 변화량을 spage_save_space(..., total_free - total_free_save)로 예약하고, 실패 시 SP_ERROR를 반환한다.
빠른 경로(spage_update_record_in_place)는 slot_p->record_length를 설정하고, 기존 offset_to_record에 memcpy한 뒤, total_free -= space를 수행한다. 크기가 줄거나 같은 갱신에서는 space <= 0이므로 이 차감이 실제로 여유 공간을 늘린다. spage_is_record_located_at_end(...)로 is_located_end를 분기한다: 영역 끝에 위치한 레코드만 cont_free -= space와 offset_to_free_area += space도 수행한다(커서가 뒤로 물러남); 그 외에는 해제된 바이트가 단편화된 상태로 남는다. 증가 경로(spage_update_record_after_compact, 6장)는 꼬리 압축, 전체 spage_compact, 또는 롤백을 수행한다.
불변식 (savings 대칭).
is_saving이면 순 여유 공간 변화량을total_free - total_free_save로spage_save_space에 전달해야 한다. 생략하면 동시 트랜잭션이 이 트랜잭션이 롤백을 위해 필요한 바이트를 소비할 수 있어 undo가 깨진다.
2.7 삭제, 툼스톤, 그리고 savings 메커니즘
섹션 제목: “2.7 삭제, 툼스톤, 그리고 savings 메커니즘”spage_delete는 성공 시 슬롯 ID를, 실패 시 NULL_SLOTID를 반환하며 anchor_type에 따라 분기한다. Figure 2-1은 분기를 완전히 나타낸다:
Figure 2-1 — spage_delete 제어 흐름
flowchart TD
B{"슬롯이 NULL?"} -->|예| R0["UNKNOWN_SLOTID; NULL_SLOTID"]
B -->|아니오| C["num_records--; total_free += freed"]
C --> G{"앵커?"}
G -->|ANCHORED| H["EMPTY; WILL_REUSE"]
G -->|DONT_REUSE| I["EMPTY; MARKDELETED"]
G -->|UNANCHORED| J["shift_down; freed += slot sz"]
G -->|default| R1["assert; NULL_SLOTID"]
H --> K{"is_saving?"}
I --> K
J --> K
K -->|예| L{"save_space 성공?"}
L -->|아니오| R2["NULL_SLOTID"]
L -->|예| N["dirty; slot_id 반환"]
K -->|아니오| N
total_free += free_space 크레딧은 무조건 적용된다; cont_free/커서 조정은 삭제된 레코드가 영역 끝에 있을 때만 적용된다(spage_is_record_located_at_end, 그 외에는 구멍이 압축을 기다린다). UNANCHORED 케이스는 슬롯도 회수하고(free_space += sizeof(SPAGE_SLOT)) is_saving == false를 단언한다.
spage_mark_deleted_slot_as_reusable은 vacuum 쪽에서 호출하는 flip 함수다(8장): 빈 툼스톤을 REC_DELETED_WILL_REUSE로 다운그레이드하여 spage_find_free_slot이 그 ID를 다시 내줄 수 있게 한다. 실패 분기는 두 가지로 구별된 에러 코드를 발생시킨다: 범위를 벗어난 slot_id(< 0 또는 >= num_slots) → SP_ERROR/ER_SP_UNKNOWN_SLOTID; 빈 툼스톤이 아닌 슬롯(offset_to_record == SPAGE_EMPTY_OFFSET이고 REC_MARKDELETED/REC_DELETED_WILL_REUSE 타입이어야 함), 즉 살아 있는 레코드 → SP_ERROR/ER_SP_BAD_INSERTION_SLOT(두 경우 모두 assert (false)를 먼저 호출). 툼스톤 경로만 record_type = REC_DELETED_WILL_REUSE로 설정하고 SP_SUCCESS를 반환한다.
savings 메커니즘. is_saving 페이지에서 해제된 바이트는 즉시 사용 가능하지 않다 — 롤백 시 더 큰 이전 레코드를 다시 삽입해야 할 수 있기 때문이다. spage_save_space는 해제된 바이트를 lock-free spage_Saving_hashmap(VPID를 키로, SPAGE_SAVE_HEAD 아래에 트랜잭션별 SPAGE_SAVE_ENTRY)에 예약한다. space == 0, 크래시 복구, vacuum 워커, space < 0, 또는 비활성 트랜잭션이면 NO_ERROR를 조기 반환하고; 활성 트랜잭션의 양수 savings만 엔트리를 할당하며 head->total_saved를 증가시킨다. spage_free_saved_spaces는 커밋/중단 시 tran_next_save 체인을 따라가며, first가 NULL이 되면 head를 삭제한다 — 따라서 §2.4에서 total_saved를 차감한다.
2.8 압축과 회수: ID는 고정, 오프셋은 이동
섹션 제목: “2.8 압축과 회수: ID는 고정, 오프셋은 이동”압축은 슬롯 ID를 바꾸지 않고 레코드를 붙여 모은다. spage_compact는 살아 있는 슬롯 배열을 구성하고(SPAGE_EMPTY_OFFSET 구멍 건너뜀), offset_to_record로 정렬한 뒤, 오프셋 순서로 각 레코드를 다음 정렬된 to_offset으로 memmove하고 해당 슬롯의 오프셋을 재기록한다:
// spage_compact -- src/storage/slotted_page.cmemmove ((char *) page_p + to_offset, (char *) page_p + slot_array[i]->offset_to_record, slot_array[i]->record_length);slot_array[i]->offset_to_record = to_offset; /* <- offset moves, slot id stays */to_offset += slot_array[i]->record_length;...page_header_p->total_free = SPAGE_DB_PAGESIZE - to_offset - (num_slots * sizeof (SPAGE_SLOT));page_header_p->cont_free = page_header_p->total_free; /* <- all free now contiguous */분기: num_records == 0이면 배열을 건너뛰고 to_offset을 헤더 크기로 리셋; calloc 실패 → ER_FAILED; record_type > REC_4BIT_USED_TYPE_MAX이거나 슬롯 카운트 불일치(num_records != j)는 치명적 — 후자는 ER_SP_WRONG_NUM_SLOTS를 발생시키고 logpb_fatal_error_exit_immediately_wo_flush를 호출한다.
불변식 (압축 시 슬롯 ID 보전). 압축은 어떤 슬롯의
offset_to_record도 재기록할 수 있지만, 슬롯의 인덱스는 절대 바꾸면 안 된다. OID(…, slotid)를 들고 있는 읽기 연산은spage_find_slot으로 그 인덱스를 신뢰하고 재접근한다; 인덱스를 재정렬하면 OID가 다른 행을 가리키게 된다.
spage_need_compact는 정책 게이트다: 단편화된 여유 공간이 페이지의 5% 이상일 때만 true를 반환한다(total_free - cont_free >= SPAGE_DB_PAGESIZE / 20).
spage_reclaim은 ANCHORED_DONT_REUSE_SLOTS 페이지에서 슬롯 배열을 축소한다. num_slots - 1에서 역순으로 slot_id를 순회하여(후미의 빈 슬롯이 깔끔하게 축소되도록); 빈 툼스톤(offset_to_record == SPAGE_EMPTY_OFFSET이고 REC_MARKDELETED/REC_DELETED_WILL_REUSE 타입)마다 분기한다 — 현재 마지막 슬롯(slot_id + 1 == num_slots)은 spage_reduce_a_slot로 제거하고, 그 외에는 REC_DELETED_WILL_REUSE로 다운그레이드 — is_reclaim = true로 설정한다. 루프 후 회수된 것이 있고 num_slots == 0이면 spage_initialize를 재실행한다. 회수된 것이 있으면 true를 반환한다.
2.9 챕터 요약 — 핵심 정리
섹션 제목: “2.9 챕터 요약 — 핵심 정리”- 슬롯은 4바이트 간접 참조다.
SPAGE_SLOT은offset_to_record/record_length/record_type을 14/14/4비트로 압축하며, 페이지 끝에서 역순으로 주소를 지정해 레코드 영역과 슬롯 배열이 서로를 향해 자란다. - 세 가지 카운터가 여유 공간 상태를 인코딩한다.
total_free(전체),cont_free(압축 없이 사용 가능,<= total_free),offset_to_free_area(쓰기 커서) —spage_verify_header가 모든 경계에서 불변식을 강제한다. anchor_type은 슬롯 ID 정책의 핵심 설정이다. 힙 페이지는ANCHORED_DONT_REUSE_SLOTS를 사용하므로, 삭제는REC_MARKDELETED툼스톤이 되고, vacuum이 처리하기 전까지 해당 ID는 재할당되지 않는다.- 리턴 코드는 상호 교환 불가다.
SP_DOESNT_FIT= 다른 페이지를 시도하라,SP_ERROR= 하드 실패,SP_SUCCESS= 완료;spage_has_enough_contiguous_space는 실패를 보고하기 전에 압축을 시도한다. - 압축은 바이트를 움직이고, 슬롯 ID는 움직이지 않는다.
spage_compact는cont_free == total_free가 되도록offset_to_record를 재기록하지만, 슬롯 인덱스는 불변으로 유지된다 — 안정적인 OID의 토대이다. - 삭제와 축소 갱신은 undo 공간을 빚질 수 있다.
is_saving페이지에서 해제된 바이트는spage_save_space에 예약되고, 전체 공간 검사에서 차감되며, 트랜잭션 종료 시spage_free_saved_spaces가 해제한다. - 회수는 앵커드 페이지 정리 작업이다.
spage_reclaim은 후미 툼스톤을 축소하고 나머지를 재사용 가능 상태로 다운그레이드한다;spage_need_compact는 5% 단편화 기준으로 압축을 게이팅한다.
Chapter 3: 레코드 타입과 MVCC 레코드 헤더
섹션 제목: “Chapter 3: 레코드 타입과 MVCC 레코드 헤더”Chapter 2에서 불투명한 바이트 덩어리를 담는 슬롯을 살펴봤다. 힙 레코드는 물리적으로 어떤 구조인가? 두 가지 어휘 체계가 이 역할을 나눠 맡는다. 슬롯 안의 4비트 record_type(Chapter 2의 spage_slot)은 바이트를 어떻게 해석할지를 지시한다 — 일반 레코드, forwarding 포인터, overflow 포인터, 또는 tombstone. 바디 안의 가변 크기 MVCC 레코드 헤더는 읽기 경로(Chapter 5)와 vacuum(Chapter 8)이 소비하는 버전 메타데이터(삽입/삭제 id, 이전 버전 포인터)를 담는다. MVCC가 레코드마다 stamp를 필요로 하는 이유는 동반 문서 cubrid-heap-manager.md의 “MVCC and the heap” 절을 참고하라.
3.1 아홉 가지 record_type 값
섹션 제목: “3.1 아홉 가지 record_type 값”슬롯의 record_type:4 비트필드(Chapter 2)는 storage_common.h에 정의된 열거형이다. 의미 있는 값은 아홉 개이며, 나머지 4비트 공간(REC_RESERVED_TYPE_8.._15)은 예약되어 있다.
// enum record_type — src/storage/storage_common.henum{/* Unknown record type */ REC_UNKNOWN = 0,/* Record without content, just the address */ REC_ASSIGN_ADDRESS = 1,/* Home of record */ REC_HOME = 2,/* No the original home of record. part of relocation process */ REC_NEWHOME = 3,/* Record describe new home of record */ REC_RELOCATION = 4,/* Record describe location of big record */ REC_BIGONE = 5, // ... condensed: REC_MARKDELETED = 6, REC_DELETED_WILL_REUSE = 7 ... // ... condensed: REC_RESERVED_TYPE_8 .. 15 ... REC_4BIT_USED_TYPE_MAX = REC_DELETED_WILL_REUSE, // highest live value is 7 REC_4BIT_TYPE_MAX = REC_RESERVED_TYPE_15};(열거형 주석은 소스 원문 그대로이며, 원문 문체를 보존한다.) 인코딩 내용을 기준으로 분류하면 다음과 같다(열 3 = 슬롯이 바디를 포함하는지 여부):
| 타입 | 바디? | 인코딩 내용 |
|---|---|---|
REC_HOME | 있음 | 레코드 전체가 이 슬롯에 존재 — 일반적인 경우 |
REC_NEWHOME | 있음 | REC_RELOCATION이 가리키는 재배치된 바디; OID로 직접 주소 지정 불가 |
REC_RELOCATION | forwarding | 바디가 홈 페이지를 벗어났음; 슬롯은 REC_NEWHOME을 가리키는 forward OID를 보유 |
REC_BIGONE | overflow | 레코드가 한 페이지를 초과; 슬롯은 overflow 파일의 overflow OID를 보유 |
REC_ASSIGN_ADDRESS | 없음 | OID는 예약됐지만 내용은 아직 기록되지 않음; MVCC stamping 우회(3.4) |
REC_MARKDELETED | 없음 | 슬롯 재사용이 불가한 tombstone |
REC_DELETED_WILL_REUSE | 없음 | 이후 삽입에 의해 슬롯이 재사용될 tombstone |
REC_UNKNOWN | 없음 | 센티넬 / 미초기화(RECDES_INITIALIZER); 실제 슬롯 타입으로는 절대 기록되지 않음 |
공개 OID 조회 대상으로 “live”한 타입은 네 가지다: REC_HOME, REC_RELOCATION, REC_BIGONE, REC_ASSIGN_ADDRESS. REC_NEWHOME은 REC_RELOCATION forward OID를 역참조할 때만 도달할 수 있는 live 데이터이며, 이 타입에 OID가 직접 도달하면 버그다. REC_*DELETED* 타입은 tombstone이고(Chapter 7), REC_UNKNOWN은 실제 슬롯에 기록되지 않는 센티넬이다.
flowchart TB
OID["공개 OID가 슬롯에 도달"] --> T{record_type}
T -->|REC_HOME| H["바디 여기에 존재, 헤더 인라인"]
T -->|REC_RELOCATION| R["forward OID"] --> NH["REC_NEWHOME"]
T -->|REC_BIGONE| B["overflow OID"]
T -->|REC_ASSIGN_ADDRESS| A["주소만 존재"]
T -->|MARKDELETED / WILL_REUSE| D["tombstone"]
Figure 3-1 — 슬롯 fetch 후 record-type 분기; 실제 순회는 Chapter 5에서 설명한다. Forwarding(REC_RELOCATION/REC_NEWHOME)과 overflow(REC_BIGONE)는 헤더 저장 방식이 다르다: 재배치된 REC_NEWHOME은 일반 홈 레코드처럼 인라인 가변 크기 헤더를 가지며, REC_BIGONE은 고정 최대 크기 헤더를 가진다(3.5).
3.2 mvcc_rec_header — 모든 필드
섹션 제목: “3.2 mvcc_rec_header — 모든 필드”메모리 상의 디코딩된 형태는 mvcc.h의 struct이다. 디스크 상에서는 flag 바이트(3.3)에 따라 or_mvcc_set_header / or_mvcc_get_header(src/base/object_representation_sr.c)로 pack/unpack되며, 디스크 바이트는 이 struct의 레이아웃과 동일하지 않다.
// struct mvcc_rec_header — src/transaction/mvcc.hstruct mvcc_rec_header{ INT32 mvcc_flag:8; /* MVCC flags */ INT32 repid:24; /* representation id */ int chn; /* cache coherency number */ MVCCID mvcc_ins_id; /* MVCC insert id */ MVCCID mvcc_del_id; /* MVCC delete id */ LOG_LSA prev_version_lsa; /* log address of previous version */};// MVCC_REC_HEADER_INITIALIZER zeroes flag/repid and sets NULL_CHN, MVCCID_NULL x2, LSA_INITIALIZER| 필드 | 역할 | 존재 이유 |
|---|---|---|
mvcc_flag:8 | OR_MVCC_FLAG_VALID_INSID (0x01), _VALID_DELID (0x02), _VALID_PREV_VERSION (0x04)의 비트 집합; 하위 5비트 사용 가능(OR_MVCC_FLAG_MASK = 0x1f). | 자기 기술적(self-describing): 어떤 선택적 필드가 존재하는지를 결정하여 전체 크기를 결정한다. word 0을 repid와 공유한다. |
repid:24 | 표현(스키마 버전) id. | 구 스키마 행이 올바른 레이아웃으로 읽히도록 repid를 유지한다. OR_MVCC_REPID_MASK = 0x00FFFFFF로 pack된다. |
chn | 캐시 일관성(cache coherency) 번호. | 비-MVCC 클래스에서는 유일한 버전 관리 정보로, 업데이트마다 증가하여 클라이언트 캐시가 오래된 데이터를 감지하게 한다; MVCC 클래스에서는 디코딩된 struct에 항상 존재하지만 가시성(visibility) 키는 아니다(3.6). |
mvcc_ins_id | 삽입자의 MVCCID. | 가시성 하한(Chapter 5). 디스크에는 _VALID_INSID가 설정된 경우에만 존재; 없으면 MVCCID_ALL_VISIBLE. |
mvcc_del_id | 삭제자의 MVCCID. | 가시성 상한 / tombstone 마커(Chapter 7). 디스크에는 _VALID_DELID가 설정된 경우에만 존재; 없으면 MVCCID_NULL. |
prev_version_lsa | 이전 버전의 로그 LSA. | 읽기 경로가 TOO_NEW_FOR_SNAPSHOT 시 따라가는 역방향 포인터이며, vacuum이 정리에 활용한다. 디스크에는 _VALID_PREV_VERSION이 설정된 경우에만 존재. |
디스크 상에서 chn 다음 word는 delete id 또는 아무것도 없다는 것이 OR_MVCC_FLAG_VALID_DELID에 의해 결정된다 — object_representation_constants.h:168의 해당 플래그 주석에 따르면: “The record contains MVCC delete id. If not set, the record contains chn”. 이 chn/del_id 중첩은 순전히 디스크 인코딩 수준의 이야기이며(CUBRID의 MVCC_* 매크로는 delid_chn 뷰를 사용한다, 예: mvcc.h의 MVCC_IS_REC_DELETED_BY), chn과 mvcc_del_id를 별도의 항상-존재하는 필드로 담는 메모리 상의 디코딩된 struct와는 구별된다.
불변: flag 바이트 하나가 헤더 크기를 결정한다.
or_header_size는mvcc_header_size_lookup[OR_GET_MVCC_FLAG(ptr)]이다 — 길이 필드나 종료자가 없으므로, 읽는 쪽과 쓰는 쪽은 반드시 flag→size 매핑에 합의해야 한다.mvcc_ins_id를 설정하면서OR_MVCC_FLAG_VALID_INSID를 빠뜨린 writer는 다음 reader가 헤더를 8바이트 짧게 계산하여 attribute 데이터를 insert id로 잘못 읽게 만든다.
3.3 Flag 기반 가변 크기 인코딩: mvcc_header_size_lookup
섹션 제목: “3.3 Flag 기반 가변 크기 인코딩: mvcc_header_size_lookup”선택적 id는 flag가 설정된 경우에만 존재하므로 헤더 크기는 여덟 가지다; 테이블은 flag→size 매핑을 O(1)로 만든다:
// mvcc_header_size_lookup — src/object/object_representation.cint mvcc_header_size_lookup[8] = { OR_MVCC_REP_SIZE + OR_CHN_SIZE, // index 0 OR_MVCC_REP_SIZE + OR_CHN_SIZE + OR_MVCCID_SIZE, // index 1 (+INSID) // ... condensed: indices 2..6 sum REP+CHN with the flagged MVCCID/LSA terms ... OR_MVCC_REP_SIZE + OR_CHN_SIZE + OR_MVCCID_SIZE + OR_MVCCID_SIZE + OR_MVCC_PREV_VERSION_LSA_SIZE};필수 rep+chn 접두사는 8바이트이며, 각 MVCCID와 prev-version LSA는 8바이트를 추가한다. 따라서 flag 인덱스 0..7에 대한 여덟 항목의 값은 순서대로 8, 16, 16, 24, 16, 24, 24, 32바이트다. 이 테이블은 카운트가 아닌 비트마스크를 인덱스로 사용한다: 인덱스 1과 2는 둘 다 16바이트(어느 쪽이든 MVCCID 하나), 4/6도 마찬가지다. 오프셋은 위치 기반으로 계산된다 — OR_MVCC_DELETE_ID_OFFSET(flags)는 VALID_INSID가 설정된 경우에만 OR_MVCC_INSERT_ID_SIZE를 더한다 — 최솟값은 OR_MVCC_MIN_HEADER_SIZE = 8, 최댓값은 OR_MVCC_MAX_HEADER_SIZE = 32. 디스크에서 8바이트 접두사(repid+flags, 이어서 chn) 다음에 mvcc_ins_id, mvcc_del_id, prev_version_lsa(각 8B)가 flagged된 것만 이 순서로 온다; attribute 데이터는 or_header_size(ptr) 위치에서 시작된다.
3.4 heap_insert_adjust_recdes_header — 배치 전 헤더 stamping
섹션 제목: “3.4 heap_insert_adjust_recdes_header — 배치 전 헤더 stamping”heap_insert_adjust_recdes_header는 클라이언트가 제공한 레코드(bare 헤더)를 stamped 레코드로 변환한다: MVCC 클래스에는 insert-id 추가, 비-MVCC 클래스에는 flag 제거, prev-version 초기화, 길이 조정. 두 가지 경로가 있다. 빠른 경로는 use_optimization을 게이트로 사용하며 조건은 is_mvcc_class && update_in_place == UPDATE_INPLACE_NONE && !VALID_PREV_VERSION && !heap_is_big_length(record_size + OR_MVCCID_SIZE) && !is_bulk_op (SERVER_MODE 전용)이다.
Branch A — use_optimization. unpack/repack 왕복을 건너뛰고 INSID를 바디에 직접 기록하는 단순 MVCC 삽입 경로다.
// heap_insert_adjust_recdes_header (Branch A) — src/storage/heap_file.cassert (!(mvcc_flags & OR_MVCC_FLAG_VALID_DELID)); /* <- a fresh insert is never pre-deleted */mvcc_id = logtb_get_current_mvccid (thread_p);new_ins_mvccid_pos_p = start_p + OR_MVCC_INSERT_ID_OFFSET;if (!(mvcc_flags & OR_MVCC_FLAG_VALID_INSID)) { repid_and_flag_bits |= (OR_MVCC_FLAG_VALID_INSID << OR_MVCC_FLAG_SHIFT_BITS); OR_PUT_INT (start_p, repid_and_flag_bits); /* <- set flag in word 0 */ memmove (new_ins_mvccid_pos_p + OR_MVCCID_SIZE, new_ins_mvccid_pos_p, insert_context->recdes_p->length - OR_MVCC_INSERT_ID_OFFSET); /* <- open 8-byte gap */ insert_context->recdes_p->length += OR_MVCCID_SIZE; }OR_PUT_BIGINT (new_ins_mvccid_pos_p, &mvcc_id); /* <- write INSID into the gap */return NO_ERROR;서브 분기: INSID flag가 이미 설정되어 있으면 8바이트 공간이 이미 존재하므로, memmove와 길이 증가를 건너뛰고 id만 덮어쓴다(Chapter 4에서 여분의 8바이트를 미리 버퍼에 확보해 둔다).
Branch B — 일반 경로. 최적화 전제조건 중 하나라도 충족되지 않을 때 진입한다(비-MVCC 클래스, in-place 업데이트, prev-version 존재, big 레코드가 될 경우, bulk 작업, 클라이언트 모드); 먼저 헤더를 완전히 디코딩한다.
// heap_insert_adjust_recdes_header (Branch B) — src/storage/heap_file.cor_mvcc_get_header (insert_context->recdes_p, &mvcc_rec_header); // ... err check condensed ...if (insert_context->update_in_place != UPDATE_INPLACE_OLD_MVCCID) { if (is_mvcc_class && !insert_context->is_bulk_op) /* B1: MVCC class */ { mvcc_id = logtb_get_current_mvccid (thread_p); if (!MVCC_IS_FLAG_SET (&mvcc_rec_header, OR_MVCC_FLAG_VALID_INSID)) { MVCC_SET_FLAG (&mvcc_rec_header, OR_MVCC_FLAG_VALID_INSID); record_size += OR_MVCCID_SIZE; } MVCC_SET_INSID (&mvcc_rec_header, mvcc_id); } else /* B2: non-MVCC / client */ { curr_header_size = mvcc_header_size_lookup[mvcc_rec_header.mvcc_flag]; MVCC_CLEAR_ALL_FLAG_BITS (&mvcc_rec_header); /* <- strip all optional ids */ new_header_size = mvcc_header_size_lookup[mvcc_rec_header.mvcc_flag]; record_size -= (curr_header_size - new_header_size); /* <- shrink length to match */ } }else if (MVCC_IS_HEADER_DELID_VALID (&mvcc_rec_header)) /* B3: redistribute keeps DELID */ insert_context->is_redistribute_insert_with_delid = true;
MVCC_CLEAR_FLAG_BITS (&mvcc_rec_header, OR_MVCC_FLAG_VALID_PREV_VERSION); /* always: new row has no prev */if (is_mvcc_class && heap_is_big_length (record_size)) HEAP_MVCC_SET_HEADER_MAXIMUM_SIZE (&mvcc_rec_header); /* <- big record -> full 32B header */or_mvcc_set_header (insert_context->recdes_p, &mvcc_rec_header); // ... err check condensed ...상호 배타적인 네 가지 결과: B1은 INSID를 stamp하고 MVCCID 하나만큼 크기를 늘린다; B2는 모든 선택적 id를 제거하고 lookup 테이블 차이만큼 정확히 줄인다; B3(파티션 redistribute)은 기존 DELID를 보존해야 함을 기록하기만 한다; UPDATE_INPLACE_OLD_MVCCID는 세 경우 모두를 그대로 통과한다. 이후 분기와 무관하게, prev-version이 초기화되고 big 레코드는 최대 크기로 승격된다(3.5).
SM_CLASS root-class 경로 vs ATTRINFO 일반 테이블 경로. 이 함수는 일반 테이블 행에 대해서만 실행된다. 삽입 드라이버는 !OID_ISNULL(class_oid) && !OID_IS_ROOTOID(class_oid) && recdes_p->type != REC_ASSIGN_ADDRESS 게이트 아래서 호출하므로 두 종류가 조정을 건너뛴다: root-class 행(OID_IS_ROOTOID — 자체 객체 헤더를 가진 raw 직렬화된 SM_CLASS 레코드는 MVCC 버전 관리 대상이 아니므로, insert-id를 삽입하면 데이터가 손상된다)과 REC_ASSIGN_ADDRESS 플레이스홀더. ATTRINFO 경로(테이블 행, heap_attrinfo 빌드 descriptor)는 비-root, 비-플레이스홀더 분기로 조정자가 실제로 호출된다.
flowchart TB
IN["삽입 드라이버"] --> Q{"root class?\n또는 REC_ASSIGN_ADDRESS?"}
Q -->|"예: SM_CLASS raw 레코드"| SKIP["조정 건너뜀"]
Q -->|"아니오: ATTRINFO 테이블 행"| OPT{use_optimization?}
OPT -->|예| A["Branch A: 빠른 INSID stamp"]
OPT -->|아니오| B["Branch B: 완전한 get/set 헤더"]
Figure 3-2 — 호출자 게이트와 헤더 조정의 내부 분기.
3.5 Overflow 레코드는 고정 크기 헤더를 유지한다
섹션 제목: “3.5 Overflow 레코드는 고정 크기 헤더를 유지한다”REC_BIGONE 바디는 overflow 파일에 존재하며, MVCC 헤더는 첫 번째 overflow 페이지에 항상 최대 크기로 기록되어 in-place로 업데이트된다. 두 헬퍼 함수가 이를 강제한다. heap_get_mvcc_rec_header_from_overflow는 OR_MVCC_MAX_HEADER_SIZE 크기의 recdes로 읽어 온다:
// heap_get_mvcc_rec_header_from_overflow — src/storage/heap_file.cpeek_recdes->data = overflow_get_first_page_data (ovf_page);peek_recdes->length = OR_MVCC_MAX_HEADER_SIZE; /* <- always read 32B */return or_mvcc_get_header (peek_recdes, mvcc_header);getter의 분기는 하나뿐이다: peek_recdes == NULL이면 로컬 ovf_recdes를 사용하고, 그렇지 않으면 호출자의 recdes에 채워서 overflow 바디까지 접근할 수 있게 한다. heap_set_mvcc_rec_header_on_overflow는 슬롯 길이가 변하지 않도록 최대 크기를 강제하여 다시 기록한다:
// heap_set_mvcc_rec_header_on_overflow — src/storage/heap_file.covf_recdes.area_size = ovf_recdes.length = OR_HEADER_SIZE (ovf_recdes.data);assert (ovf_recdes.length == OR_MVCC_MAX_HEADER_SIZE); /* <- existing header must already be 32B */if (!MVCC_IS_FLAG_SET (mvcc_header, OR_MVCC_FLAG_VALID_INSID)) { MVCC_SET_FLAG_BITS (mvcc_header, OR_MVCC_FLAG_VALID_INSID); MVCC_SET_INSID (mvcc_header, MVCCID_ALL_VISIBLE); } /* <- force INSID present */if (!MVCC_IS_FLAG_SET (mvcc_header, OR_MVCC_FLAG_VALID_DELID)) { MVCC_SET_FLAG_BITS (mvcc_header, OR_MVCC_FLAG_VALID_DELID); MVCC_SET_DELID (mvcc_header, MVCCID_NULL); } /* <- force DELID present */assert (mvcc_header_size_lookup[MVCC_GET_FLAG (mvcc_header)] == OR_MVCC_MAX_HEADER_SIZE);return or_mvcc_set_header (&ovf_recdes, mvcc_header);두 if 블록이 유일한 분기다: 이미 설정된 flag는 그대로 두고, 없는 INSID/DELID flag는 중립적인 값(MVCCID_ALL_VISIBLE, MVCCID_NULL)으로 추가된다. HEAP_MVCC_SET_HEADER_MAXIMUM_SIZE(3.4)는 삽입 시점에 동일한 작업을 수행한다.
불변: overflow 헤더는 항상 정확히
OR_MVCC_MAX_HEADER_SIZE(32바이트)다. 두 헬퍼 모두 이를 assert로 검증한다. DELID를 stamp하는 등 헤더를 in-place로 업데이트할 때 바디가 이동해서는 안 된다; 고정 32바이트 헤더 덕분에 레코드 길이가 변하지 않는다. 가변 크기 헤더라면 DELID stamp 때마다 수 메가바이트에 달할 수 있는 레코드의 모든 바이트를 이동시켜야 한다.
3.6 chn vs mvcc_ins_id
섹션 제목: “3.6 chn vs mvcc_ins_id”이미 앞에서 언급했다(3.2의 chn 행, 3.4의 Branch B2): 비-MVCC 클래스는 모든 MVCC flag를 제거하여 8바이트 rep+chn 형태로 저장하며, chn만이 일관성을 담당한다(MVCC_IS_CHN_UPTODATE로 확인); MVCC 클래스는 mvcc_ins_id를 가시성 키로 stamp하는 반면 chn은 존재하지만 비활성 상태다. 디코더(or_mvcc_get_header)는 양쪽 모두를 처리한다.
3.7 챕터 요약 — 핵심 정리
섹션 제목: “3.7 챕터 요약 — 핵심 정리”- 4비트
record_type이 첫 번째 분기 기준이다: 네 가지 타입(REC_HOME,REC_RELOCATION,REC_BIGONE,REC_ASSIGN_ADDRESS)이 공개 OID가 도달할 수 있는 대상이며,REC_NEWHOME은REC_RELOCATIONforward를 거쳐야만 도달 가능하고,REC_*DELETED*는 tombstone이며,REC_UNKNOWN은 센티넬이다. - Forwarding(
REC_NEWHOME)은 인라인 가변 크기 헤더를 가지며, overflow(REC_BIGONE)는 고정 32바이트 헤더를 가진다. mvcc_rec_header는 flags+repid를 하나의 word에 pack하고, 항상 존재하는chn과 8비트mvcc_flag로 제어되는 세 가지 선택적 디스크 필드(mvcc_ins_id,mvcc_del_id,prev_version_lsa)를 담는다.- 헤더 크기는 flag 바이트만으로 결정되는 순수 함수다 —
or_header_size는mvcc_header_size_lookup[flag]를 인덱싱한다; 길이 필드가 없으므로 writer와 reader는 반드시 flag에 합의해야 하며, 그렇지 않으면 파싱 오류가 발생한다. heap_insert_adjust_recdes_header는 빠른 경로(Branch A: memmove로 INSID stamp)와 일반 경로(Branch B: B1 MVCC-stamp, B2 비-MVCC를 위한 strip, B3 redistribute-keep-DELID)를 가지며, 항상 prev-version을 초기화하고 big 레코드를 최대 크기로 승격한다. Root-classSM_CLASS레코드와REC_ASSIGN_ADDRESS플레이스홀더는 조정을 건너뛰며, non-root ATTRINFO 행만 stamp된다.chn은 비-MVCC 클래스(8바이트 헤더)의 일관성 키이고,mvcc_ins_id는 MVCC 클래스의 가시성 키다.
Chapter 4: 삽입 흐름과 OID 할당
섹션 제목: “Chapter 4: 삽입 흐름과 OID 할당”이 챕터는 레코드 한 건의 탄생 과정을 추적한다 — 호출자의 RECDES에서
출발해 OID가 발급되고 X-락이 걸린 뒤 바이트가 슬롯 페이지에 기록되기까지:
OID는 어떻게 발급되고, 홈 페이지는 어떻게 선택되며, 크기를 초과한 레코드는
삽입 시점에 어떻게 오버플로로 분리되는가? 배치 알고리즘 개요는 고수준
동반 문서의 ### Insert flow 절을 참고한다. 여기서는 모든 분기를
따라간다. 레코드 타입은 챕터 3, HEAP_OPERATION_CONTEXT는 챕터 1,
best-space 선택은 챕터 9, 로깅은 챕터 10에서 다룬다.
4.1 컨텍스트 구성 — heap_create_insert_context
섹션 제목: “4.1 컨텍스트 구성 — heap_create_insert_context”모든 논리적 힙 연산은 HEAP_OPERATION_CONTEXT 하나를 매개로 실행된다.
삽입용 생성자는 컨텍스트를 초기화한 뒤 입력값을 채운다:
// heap_create_insert_context -- src/storage/heap_file.cheap_clear_operation_context (context, hfid_p); /* <- reset ALL fields; flag defaults below */if (class_oid_p != NULL) { COPY_OID (&context->class_oid, class_oid_p); } /* <- may stay NULL */context->recdes_p = recdes_p; /* <- caller's record bytes + type */context->scan_cache_p = scancache_p; /* <- optional page-caching hint */context->type = HEAP_OPERATION_INSERT;heap_clear_operation_context는 res_oid, ovf_oid, map_recdes와
아래 세 가지 동작 플래그를 모두 (= false로) 초기화한다:
| 플래그 | 설정 위치 | 삽입에 미치는 효과 |
|---|---|---|
is_logical_old | 삽입 시에는 false; heap_update_logical에서 재배치 원본에만 true | false = 순수하게 새로운 논리 객체; REC_NEWHOME 재배치는 heap_insert_newhome을 사용하므로 이 경로에서는 절대 true가 되지 않음 |
is_redistribute_insert_with_delid | heap_insert_adjust_recdes_header, 입력 헤더에 유효한 DELID가 이미 있을 때 | 로깅을 heap_mvcc_log_redistribute로 전환 (§4.7) |
is_bulk_op | heap_insert_logical 호출 전 bulk-load 호출자가 설정 | INSID 스탬핑 억제, X_LOCK 대신 NULL_LOCK, 사전 보유 BU_LOCK 단언 |
불변식 — 컨텍스트는 단일 사용이며 완전히 초기화된다.
heap_create_insert_context는 항상 초기화를 먼저 수행하므로, 이전
연산에서 남은 값이 남아 있지 않는다. 초기화 없이 재사용하면 낡은
ovf_oid/res_oid가 남아 포워딩 맵이 손상된다.
4.2 전체 흐름 — heap_insert_logical
섹션 제목: “4.2 전체 흐름 — heap_insert_logical”heap_insert_logical은 단일 진입점이다 (Figure 4-1, 모든 분기가
error:로 귀결). 그림에 없는 결정이 하나 있다: MVCC 연산 분류 —
is_mvcc_op는 SERVER_MODE이며 MVCC가 활성화된 클래스, REC_ASSIGN_ADDRESS가
아닌 레코드, 그리고 bulk가 아닌 경우에만 true가 된다. 이 값은 로깅(§4.7)
에서만 소비되며 배치 결정에는 영향을 주지 않는다. 헤더 조정 게이트(§4.3)는
루트 클래스, NULL 클래스 OID, REC_ASSIGN_ADDRESS에 대해서는 건너뛴다.
flowchart TB
A["heap_insert_logical"] --> B{"scancache_check OK?"}
B -- "아니오" --> RF1["ER_FAILED 반환 (페이지 미고정)"]
B -- "예" --> G{"class != NULL, ROOT 아님,\ntype != REC_ASSIGN_ADDRESS?"}
G -- "예, 실패" --> RF1
G -- "예, 성공 / 아니오" --> J["heap_insert_handle_multipage_record"]
J -- "ER_FAILED" --> ERR["goto error"]
J -- "ok" --> M["클래스 락: bulk → BU_LOCK 단언,\n그 외 → IX_LOCK UNCOND"]
M -- "미승인" --> RF1
M -- "승인" --> Q["heap_get_insert_location_with_lock"]
Q -- "실패" --> RF1
Q -- "ok, res_oid 설정 + 락 획득" --> O["heap_insert_physical"]
O -- "실패" --> ERR
O -- "ok" --> U["bulk 아니면 로그 기록; set_dirty;\ncache 또는 pgbuf_ordered_unfix; perfmon"]
U --> ERR
ERR["error:"] --> RET["rc 반환"]
Figure 4-1 — 분기 완전 제어 흐름. 초기 실패는 직접 반환 (페이지
미고정); 고정 이후 실패는 goto error (공유 SystemTap 종료점). error:는
정상 종료(성공) 경로이기도 하다.
4.3 MVCC INSID 스탬핑 — heap_insert_adjust_recdes_header
섹션 제목: “4.3 MVCC INSID 스탬핑 — heap_insert_adjust_recdes_header”MVCC 클래스의 경우 여기서 행에 삽입 MVCCID가 찍힌다. 빠른 경로 (아래 가드 조건)와 일반 경로가 있다:
// heap_insert_adjust_recdes_header -- src/storage/heap_file.cuse_optimization = (is_mvcc_class && update_in_place == UPDATE_INPLACE_NONE && !(mvcc_flags & OR_MVCC_FLAG_VALID_PREV_VERSION) && !heap_is_big_length (record_size + OR_MVCCID_SIZE) && !is_bulk_op);if (use_optimization) { /* <- in-place: OR_PUT_INT INSID flag; memmove 8-byte gap; */ return NO_ERROR; /* length += OR_MVCCID_SIZE; OR_PUT_BIGINT INSID; one memmove, no pack/unpack */}빠른 경로는 일반적인 경우(새로운, 큰 레코드가 아닌, prev-version 없는
삽입)에 해당한다. 일반 경로(or_mvcc_get_header → 변형 →
or_mvcc_set_header)에는 세 가지 서브 분기가 있다: MVCC, 비-bulk —
OR_MVCC_FLAG_VALID_INSID가 없으면 설정, OR_MVCCID_SIZE만큼 확장,
MVCC_SET_INSID; 비-MVCC 또는 bulk — 모든 플래그 제거
(MVCC_CLEAR_ALL_FLAG_BITS) 후 축소; UPDATE_INPLACE_OLD_MVCCID —
기존 MVCCID 유지, DELID가 존재하면 is_redistribute_insert_with_delid 설정
(파티션 재배분). 이후 큰 MVCC 레코드는 헤더를 최대 크기로 강제
설정(HEAP_MVCC_SET_HEADER_MAXIMUM_SIZE)하여, 이후 오버플로 맵의 in-place
삭제/갱신 시 홈 슬롯이 커지지 않도록 한다.
불변식 — INSID 폭은 배치 전에 예약된다. §4.5의 홈 페이지 크기는
OR_MVCCID_SIZE가 추가된 이후의 recdes_p->length를 기준으로 산정하며,
빠른 경로의 area_size >= length + OR_MVCCID_SIZE 단언이 memmove 범위를
보장한다.
4.4 크기 초과 분리 — heap_insert_handle_multipage_record
섹션 제목: “4.4 크기 초과 분리 — heap_insert_handle_multipage_record”이 함수는 “페이지에 들어간다”와 “오버플로로 간다” 사이의 게이트다:
// heap_insert_handle_multipage_record -- src/storage/heap_file.cif (!heap_is_big_length (context->recdes_p->length)) { return NO_ERROR; } /* <- normal: untouched */if (heap_ovf_insert (thread_p, &context->hfid, &context->ovf_oid, context->recdes_p) == NULL) { return ER_FAILED; } /* <- overflow insert failed */heap_build_forwarding_recdes (&context->map_recdes, REC_BIGONE, &context->ovf_oid);context->recdes_p = &context->map_recdes; /* <- home page now receives the 8-byte map */heap_is_big_length (length > heap_Maxslotted_reclength)는 “크기 초과”
여부의 단일 판정 기준이다. 크지 않으면 이 함수는 아무것도 하지 않는다.
크면 heap_build_forwarding_recdes가 8바이트 REC_BIGONE 맵을 만들고
context->recdes_p를 해당 맵으로 교체한다. 이후 heap_insert_logical의
나머지 부분은 이 작은 레코드를 일반 레코드처럼 배치한다.
불변식 — MVCC 헤더는 오버플로 레코드에 존재하며 맵에는 없다.
소스 코드 주석에 명시되어 있다 (“MVCC information is held in overflow
record”). 이것이 §4.3에서 오버플로 레코드 헤더를 최대 크기로 강제한
이유다. 홈 페이지의 REC_BIGONE 맵은 MVCCID를 갖지 않는다. 가시성
(챕터 5)은 OID를 따라 오버플로로 이동한다.
4.4.1 heap_ovf_insert
섹션 제목: “4.4.1 heap_ovf_insert”// heap_ovf_insert -- src/storage/heap_file.cif (heap_ovf_find_vfid (thread_p, hfid, &ovf_vfid, true, PGBUF_UNCONDITIONAL_LATCH) == NULL || overflow_insert (thread_p, &ovf_vfid, &ovf_vpid, recdes, FILE_MULTIPAGE_OBJECT_HEAP) != NO_ERROR) { return NULL; } /* <- either branch fails -> NULL */ovf_oid->pageid = ovf_vpid.pageid; ovf_oid->volid = ovf_vpid.volid;ovf_oid->slotid = NULL_SLOTID; /* <- overflow has no slot */true 인수는 오버플로 파일이 없으면 생성한다. ovf_oid는 첫 번째
오버플로 페이지를 식별한다 (슬롯 필드는 무의미 — 오버플로 페이지는
페이지 체인 구조이며 슬롯 방식이 아니다).
4.5 홈 페이지 선택과 OID 발급
섹션 제목: “4.5 홈 페이지 선택과 OID 발급”recdes_p가 준비되고 클래스 IX 락이 획득되면, heap_insert_logical은
heap_get_insert_location_with_lock을 호출한다 — OID가 탄생하는 지점:
- 페이지 선택.
home_hint == NULL→heap_stats_find_best_page(isnew_rec = true, 챕터 9의 블랙박스 — 고정된 페이지 또는 §4.6 폴백을 통한 오류 반환, “공간 없음”은 없음;NULL→return error_code). 힌트가 있으면 → 너무 작으면ER_SP_NOSPACE_IN_PAGE, 그렇지 않으면 채택. 어느 경우든res_oid.volid/pageid가 설정된다. - 락 모드:
SCH_M_LOCK(루트),NULL_LOCK(bulk), 그 외X_LOCK. - 슬롯 루프 (
slot 0..slot_count):spage_find_free_slot이slot_id를 반환하거나 (REC_DELETED_WILL_REUSE슬롯 재활용 포함, 챕터 7;== slot_count는 “추가”),SP_ERROR→ break.res_oid.slotid설정;NULL_LOCK이면 즉시 반환, 그 외lock_object조건부 시도:LK_GRANTED→ 반환;LK_NOTGRANTED_DUE_TIMEOUT→ 다음 슬롯; 그 외 오류 → break. - Break 경로:
res_oid무효화, 페이지 unfix,assert(false),return ER_FAILED.
불변식 — OID는 바이트가 기록되기 전에 완전히 결정되고 X-락이 걸린다.
res_oid는 성공적인 lock_object (또는 NULL_LOCK bulk의 경우 즉시) 이후에만
완성된다. 아직 기록되지 않은 슬롯에 대한 락이 먼저 걸리므로, 행 락은
INSERT 내부에서 획득된다. 3단계의 조건부 재시도 덕분에 두 삽입자가
임시 슬롯을 두고 데드락에 빠지는 일은 없다.
4.6 물리적 배치 — heap_insert_physical과 heap_alloc_new_page
섹션 제목: “4.6 물리적 배치 — heap_insert_physical과 heap_alloc_new_page”heap_insert_physical은 미리 예약된 슬롯에 정확히 기록한다 — 슬롯을
직접 선택하지 않는다:
// heap_insert_physical -- src/storage/heap_file.cassert (context->res_oid.slotid != NULL_SLOTID); /* <- slot chosen in §4.5 */if (spage_insert_at (thread_p, context->home_page_watcher_p->pgptr, context->res_oid.slotid, context->recdes_p) != SP_SUCCESS) { er_set (ER_FATAL_ERROR_SEVERITY, ...); OID_SET_NULL (&context->res_oid); return ER_FAILED; }spage_insert_at은 챕터 2의 기본 연산이다. 실패는 치명적이다 (슬롯이
방금 여유 있고 락 가능함을 검증했으므로 SP_DOESNT_FIT은 페이지 손상을
의미한다).
heap_alloc_new_page는 best-space 뒤의 새 페이지 폴백으로, 여유 공간이
있는 페이지가 없을 때 heap_stats_find_best_page(와 bulk 로더)가 호출한다.
heap_insert_logical에서 직접 호출하지 않는다:
// heap_alloc_new_page -- src/storage/heap_file.cHEAP_PAGE_SET_VACUUM_STATUS (&new_page_chain, HEAP_PAGE_VACUUM_NONE); /* <- clean page, links nulled */error_code = file_alloc (thread_p, &hfid->vfid, heap_vpid_init_new, &new_page_chain, new_page_vpid, &page_ptr);if (error_code != NO_ERROR) { ASSERT_ERROR (); return error_code; } /* <- no watcher attached yet */pgbuf_attach_watcher (thread_p, page_ptr, PGBUF_LATCH_WRITE, hfid, home_hint_p);새로운 HEAP_CHAIN 헤더(챕터 8)를 초기화한 뒤 쓰기 latch 상태의 watcher를
붙이고 페이지를 고정한 채로 반환한다. 오류 경로는 §4.2의 비대칭성을 그대로
따른다: file_alloc 실패 시 watcher가 붙기 전에 ASSERT_ERROR()로
오류를 반환하여 고정된 페이지가 남지 않는다. (HEAP_CHAIN 필드: 챕터 1.)
4.7 로깅 훅 포인트
섹션 제목: “4.7 로깅 훅 포인트”성공적인 삽입 이후 (use_bulk_logging이 아닌 경우),
heap_log_insert_physical이 레코드 타입과 연산 종류(is_mvcc_op와
is_redistribute_insert_with_delid 사용)에 따라 분기한다:
- MVCC + redistribute →
heap_mvcc_log_redistribute. - MVCC, 일반 →
heap_mvcc_log_insert. - 비-MVCC,
REC_ASSIGN_ADDRESS→RVHF_INSERTundoredo, 2바이트 예약 길이 페이로드 (본문 없음). - 비-MVCC,
REC_NEWHOME→RVHF_INSERT_NEWHOME. - 비-MVCC, 그 외 → 단순
RVHF_INSERT.
RVHF_* 의미론은 챕터 10에서 다룬다. Perfmon 카운터
(PSTAT_HEAP_HOME_INSERTS / BIG_INSERTS / ASSIGN_INSERTS)는 최종
recdes_p->type을 기준으로 분류된다 (§4.4에서 분리된 경우 REC_BIGONE).
4.8 사전 발급 경로 — heap_assign_address
섹션 제목: “4.8 사전 발급 경로 — heap_assign_address”heap_assign_address는 행의 바이트가 존재하기 전에 OID를 예약한다
(변환기가 순환 참조를 끊는 데 사용한다).
// heap_assign_address -- src/storage/heap_file.cif (expected_length <= 0) { /* ... heap_estimate_avg_length ... */ }recdes.length = /* <- clamp to [OID_SIZE, non-big] */ ((expected_length > SSIZEOF (OID) && !heap_is_big_length (expected_length)) ? expected_length : SSIZEOF (OID));recdes.data = NULL; recdes.type = REC_ASSIGN_ADDRESS; /* <- placeholder type */heap_create_insert_context (&insert_context, (HFID *) hfid, class_oid, &recdes, NULL);rc = heap_insert_logical (thread_p, &insert_context, NULL);COPY_OID (oid, &insert_context.res_oid); /* <- hand back the minted OID */길이를 알 수 없을 때는 힙의 평균 객체 길이로 폴백한다 (큰 예약은
OID_SIZE만 예약하며 내용은 나중에 오버플로로 이동). REC_ASSIGN_ADDRESS
타입은 삽입이 헤더 조정(§4.2)을 건너뛰고 비-MVCC(§4.7)로 로그를 남기게
한다. 슬롯은 이후 UPDATE(챕터 6)가 채운다.
4.9 공유 재배치 헬퍼 — heap_insert_newhome
섹션 제목: “4.9 공유 재배치 헬퍼 — heap_insert_newhome”heap_insert_newhome은 REC_NEWHOME 본문 — 홈 페이지를 벗어난 레코드의
재배치 복사본 — 을 배치한다. 논리적 삽입 경로에서는 절대 호출되지 않으며,
갱신(챕터 6)과 삭제 재배치가 재사용한다.
// heap_insert_newhome -- src/storage/heap_file.cassert (parent_context->type == HEAP_OPERATION_DELETE || parent_context->type == HEAP_OPERATION_UPDATE);heap_create_insert_context (&ins_context, &parent_context->hfid, &parent_context->class_oid, recdes_p, NULL);error_code = heap_find_location_and_insert_rec_newhome (thread_p, &ins_context); /* <- find page + spage_insert */heap_log_insert_physical (thread_p, ..., &ins_context.res_oid, ins_context.recdes_p, false, false); /* <- always non-MVCC */if (out_oid_p != NULL) { COPY_OID (out_oid_p, &ins_context.res_oid); } /* <- give caller the OID */if (newhome_pg_watcher != NULL) /* <- optional: hand page back */ pgbuf_replace_watcher (thread_p, ins_context.home_page_watcher_p, newhome_pg_watcher);수정자 관점에서 중요한 차이점: 자체 자식 ins_context를 구성한다
(§4.1의 단일 사용 불변식); 행 락을 걸지 않는다 (REC_NEWHOME은
REC_RELOCATION 포인터로만 접근되므로, 가시적인 OID와 X-락은
거기에 있다. 배치는 isnew_rec = false로 heap_stats_find_best_page를
사용한 뒤 spage_insert — 페이지 선택 방식, 사전 락 없음); 로깅은
항상 비-MVCC(RVHF_INSERT_NEWHOME, vacuum이 REC_NEWHOME을 검사하지
않으므로); newhome_pg_watcher가 NULL이 아니면 pgbuf_replace_watcher로
페이지를 고정 상태로 유지하여 호출자가 재고정 없이 prev-version LSA를
설정할 수 있다(챕터 6).
4.10 챕터 요약 — 핵심 정리
섹션 제목: “4.10 챕터 요약 — 핵심 정리”- 삽입은 하나의 단일 경로다.
heap_insert_logical이 단일 사용 컨텍스트를 헤더 조정 → 분리 → 페이지 선택+락 → 물리적 기록 → 로그 순서로 진행한다. 초기 실패는 직접 반환, 고정 이후 실패는goto error. - MVCCID는 배치 전에 스탬핑된다.
heap_insert_adjust_recdes_header가 8바이트 INSID를 추가(빠른 in-place 경로)하므로 페이지 크기 산정이 최종 길이를 기준으로 이루어진다. 비-MVCC/bulk는 모든 플래그를 제거한다. - 크기 초과는 작은 홈 레코드를 의미한다.
heap_insert_handle_multipage_record가heap_ovf_insert로 바이트를 오버플로에 쓰고, MVCC 헤더가 오버플로 레코드에 있는 8바이트REC_BIGONE맵을 남긴다. - OID는 바이트가 기록되기 전에 발급되고 X-락이 걸린다.
heap_get_insert_location_with_lock이 페이지를 고정하고res_oid를 완성하며 임시 슬롯을 조건부 X-락으로 잠근다. 경합 시 다음 슬롯으로 이동. - 물리적 기록은 예약을 신뢰한다.
heap_insert_physical이 사전 락된 슬롯에spage_insert_at을 호출한다 (실패는 치명적).heap_alloc_new_page는 폴백으로,file_alloc실패 시 고정된 페이지를 남기지 않는다. - 두 가지 특수 탄생 경로.
heap_assign_address는 본문 없는 OID를 발급한다 (REC_ASSIGN_ADDRESS, 이후 UPDATE가 채움).heap_insert_newhome은 재배치 전용 쌍둥이 — 자체 컨텍스트, 행 락 없음, 항상 비-MVCCRVHF_INSERT_NEWHOME, 선택적 페이지 watcher 반환.
Chapter 5: 읽기 경로 — 가시성 판정과 포워딩 체인 추적
섹션 제목: “Chapter 5: 읽기 경로 — 가시성 판정과 포워딩 체인 추적”읽기 경로가 답해야 할 질문은 하나다: 주어진 OID(또는 순차 커서)에서 이 트랜잭션이 읽을 수 있는 바이트는 무엇인가? 해당 OID의 레코드가 직접 데이터를 담고 있지 않을 수도 있다 — REC_NEWHOME 본문을 가리키는 REC_RELOCATION 포인터이거나, 오버플로우 페이지를 가리키는 REC_BIGONE 포인터일 수 있다. 위치를 찾았더라도 MVCC 스냅샷이 현재 버전을 비가시 상태로 판정하면 로그를 역방향으로 따라가야 한다. 이 챕터는 두 경로를 분기별로 추적한다.
동반 문서의 ### Read flow와 ### MVCC integration — record header에서 개념을 다루고 있으며, 스냅샷 술어(mvcc_satisfies_snapshot)는 이론적 재도출 없이 SNAPSHOT_SATISFIED / TOO_OLD_FOR_SNAPSHOT / TOO_NEW_FOR_SNAPSHOT이 이미 이해된 것으로 가정한다.
5.1 작업 공간 — HEAP_GET_CONTEXT
섹션 제목: “5.1 작업 공간 — HEAP_GET_CONTEXT”모든 읽기는 HEAP_GET_CONTEXT(heap_file.h의 struct heap_get_context)로 집약된다. 이 구조체는 OID, latch된 페이지, 레코드 타입을 헬퍼 함수 간에 전달하는 스크래치패드다. 각 필드:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
record_type | 디스패처 출력값; REC_HOME/REC_RELOCATION/REC_BIGONE/… | 타입별 분기 구동; 슬롯은 한 번만 읽힘. |
oid_p | 홈 OID (입력값, const). | 홈 페이지+슬롯; REC_NEWHOME은 절대 아님. |
forward_oid | REC_RELOCATION/REC_BIGONE일 때 채워짐. | REC_NEWHOME 본문 슬롯, 또는 첫 번째 오버플로우 페이지. |
class_oid_p | 클래스 OID; NULL일 수 있으며 페이지 체인에서 채워짐. | 해당 클래스의 MVCC 비활성화 여부 결정. |
recdes_p | 출력 바이트; 헤더만 필요할 경우 NULL. | 데이터 복사 없이 가시성 판정 수행 가능. |
scan_cache | 소유자인 HEAP_SCANCACHE, 또는 NULL. | 복사 영역 + cache_last_fix_page latch 유지. |
home_page_watcher | 홈 페이지에 대한 순서 지정 watcher. | get 전반에 걸쳐 유지; 정리 시 반환됨. |
fwd_page_watcher | 포워드/오버플로우 페이지에 대한 순서 지정 watcher. | relocation/bigone에서만 fix됨. |
ispeeking | PEEK(별칭) 또는 COPY(영역으로 복사). | 데이터가 latch 해제 이후에도 유효해야 하는지 여부 — 5.7 참조. |
old_chn | 호출자가 캐시한 CHN, 또는 NULL_CHN. | ”클라이언트가 이미 이 버전을 갖고 있음” 단락 경로. |
latch_mode | READ, 또는 scan cache가 X를 요구할 때 WRITE. | 연속 증가 읽기 시 X 모드로 re-fix를 피함. |
불변 조건 (포워드 일관성). heap_prepare_get_context가 S_SUCCESS를 반환할 때, 모든 호출자는 record_type == REC_HOME 또는 (!OID_ISNULL(&forward_oid) 이고 fwd_page_watcher.pgptr != NULL)을 단언한다 — heap_get_visible_version_internal/heap_get_last_version 상단의 assert 쌍이 이를 강제한다. 이 조건이 위반되면 heap_get_mvcc_header가 relocation/bigone 레코드의 NULL 포워드 페이지를 역참조하여 크래시가 발생한다.
5.2 홈 페이지 fix — heap_prepare_object_page
섹션 제목: “5.2 홈 페이지 fix — heap_prepare_object_page”성공 시 watcher는 해당 OID가 위치한 페이지를 보유한다.
// heap_prepare_object_page -- src/storage/heap_file.cVPID_GET_FROM_OID (&object_vpid, oid);if (page_watcher_p->pgptr != NULL && !VPID_EQ (pgbuf_get_vpid_ptr (page_watcher_p->pgptr), &object_vpid)) pgbuf_ordered_unfix (thread_p, page_watcher_p); /* <- 잘못된 페이지가 latch됨; 해제 */if (page_watcher_p->pgptr == NULL) { ret = pgbuf_ordered_fix (thread_p, &object_vpid, OLD_PAGE, latch_mode, page_watcher_p); if (ret == ER_PB_BAD_PAGEID) ret = ER_HEAP_UNKNOWN_OBJECT; /* <- 잘못된 페이지 = "객체 없음" */ if (ret == ER_LK_PAGE_TIMEOUT && er_errid () == NO_ERROR) ret = ER_PAGE_LATCH_ABORTED; }return ret;세 가지 분기: 올바른 페이지가 이미 보유됨 → fix 생략; 다른 페이지가 latch됨 → 먼저 unfix; 페이지 없음 → fix. ER_PB_BAD_PAGEID → ER_HEAP_UNKNOWN_OBJECT(호출자는 S_DOESNT_EXIST로 매핑 — 끊어진 OID는 정상 케이스); ER_LK_PAGE_TIMEOUT → ER_PAGE_LATCH_ABORTED, 순서 지정 fix 재시도 신호.
5.3 디스패처 — heap_prepare_get_context
섹션 제목: “5.3 디스패처 — heap_prepare_get_context”홈 페이지를 fix하고(5.2), 레코드 타입을 읽고, 간접 타입의 경우 포워드 페이지도 fix한다 — 읽기 경로의 분기 핵심부:
flowchart TD
A[heap_prepare_get_context] --> B[heap_prepare_object_page home]
B -->|ER_HEAP_UNKNOWN_OBJECT| Z1[S_DOESNT_EXIST]
B -->|기타 오류| ERR[goto error: 정리 + S_ERROR]
B -->|성공| E{슬롯 record_type}
E -->|NULL 슬롯 / ASSIGN_ADDRESS / MARKDELETED / DELETED_WILL_REUSE| Z2[S_DOESNT_EXIST 또는 오류]
E -->|REC_HOME| H[S_SUCCESS, 홈만]
E -->|REC_RELOCATION| R[forward_oid peek, fwd 페이지 fix]
E -->|REC_BIGONE| G[forward_oid peek, 오버플로우 페이지 fix]
E -->|REC_NEWHOME 직접 읽기| ERR2[ER_HEAP_BAD_OBJECT_TYPE]
R -->|홈 unfix됨| RT[한 번 재시도: try_again]
R -->|안정적| H
G -->|fix 성공| H
G -->|page_was_unfixed| AS[assert false]
Figure 5-2. heap_prepare_get_context의 모든 분기.
직관적이지 않은 분기들:
REC_RELOCATION재시도. 포워드 fix 이후home_page_watcher.page_was_unfixed가 순서 지정 fix가 홈을 다시 확보했음을 나타내면, 재배치 링크가 변경되었을 수 있으므로try_again으로 한 번 루프된다(try_max == 1); 두 번째 unfix는ER_PAGE_LATCH_ABORTED를 발생시킨다.REC_BIGONE의 경우 포워드 watcher가PGBUF_ORDERED_HEAP_OVERFLOW로 재순위되며 홈의page_was_unfixed는 기대되지 않는다(오버플로우는 불변) →assert(false).- 엣지 케이스. 공급되었지만 NULL인
class_oid_p는 체인 레코드에서 채워진다;REC_NEWHOME직접 읽기는 불법 →ER_HEAP_BAD_OBJECT_TYPE.error:레이블은heap_clean_get_context+S_ERROR를 실행하며;S_DOESNT_EXIST/S_SUCCESS경로는 정리하지 않는다 — 호출자가 latch를 소유한다.
5.4 헤더 읽기 — heap_get_mvcc_header
섹션 제목: “5.4 헤더 읽기 — heap_get_mvcc_header”페이지가 latch되고 타입이 확인되면 heap_get_mvcc_header는 순수한 3-way switch (context->record_type)이다: REC_HOME은 홈 슬롯을 peek하고 or_mvcc_get_header로 파싱; REC_RELOCATION은 forward_oid에서 동일하게 수행; REC_BIGONE은 heap_get_mvcc_rec_header_from_overflow를 호출; default는 assert(false) → S_ERROR. 사전 조건 단언(홈이 OID와 일치, 포워드 페이지가 forward_oid와 일치)이 5.1의 포워드 일관성 불변 조건의 실질적 보호 장치다.
5.5 가시성 판정 — heap_get_visible_version_internal
섹션 제목: “5.5 가시성 판정 — heap_get_visible_version_internal”heap_get_visible_version은 얇은 래퍼다. 내부 함수는 컨텍스트를 준비하고(5.3; S_SUCCESS가 아니면 → goto exit), 스냅샷 또는 old_chn이 있을 때 헤더를 읽은 후(5.4), 판정 결과를 매핑한다:
// heap_get_visible_version_internal -- src/storage/heap_file.csnapshot_res = mvcc_snapshot->snapshot_fnc (thread_p, &mvcc_header, mvcc_snapshot);if (snapshot_res == TOO_NEW_FOR_SNAPSHOT) /* 원하는 버전이 더 오래됨, 로그 안에 있음 */ { scan = heap_get_visible_version_from_log (..., &MVCC_GET_PREV_VERSION_LSA (&mvcc_header), ...); goto exit; }else if (snapshot_res == TOO_OLD_FOR_SNAPSHOT) /* 우리에게 죽은 버전; 미스 */ { scan = S_SNAPSHOT_NOT_SATISFIED; goto exit; }/* 그 외 SNAPSHOT_SATISFIED는 CHN 검사로 넘어감, 이후 copy (5.7) */if (MVCC_IS_CHN_UPTODATE (&mvcc_header, context->old_chn)) /* <- 스냅샷 없이도 실행 */ { scan = S_SUCCESS_CHN_UPTODATE; goto exit; }비대칭성은 의미론적으로 중요하다: TOO_OLD는 미스이지만, TOO_NEW는 그렇지 않다 — 원하는 버전이 undo 로그에 더 오래된 형태로 존재하므로 MVCC_GET_PREV_VERSION_LSA를 따른다(5.6). CHN 단락 경로는 스냅샷 블록 이후에 무조건 실행되며, old_chn이 전달된 경우 스냅샷 없이도 동작한다(### CHN). heap_get_last_version은 이 골격에서 스냅샷 블록을 제거한 것으로, 업데이터/로커를 위한 스냅샷 없는 형제 함수다.
5.6 로그로의 추적 — heap_get_visible_version_from_log
섹션 제목: “5.6 로그로의 추적 — heap_get_visible_version_from_log”TOO_NEW가 발생하면 prev_version_lsa로 연결된 undo 레코드 체인에서 이전 버전을 재구성한다:
// heap_get_visible_version_from_log -- src/storage/heap_file.cfor (LSA_COPY (&process_lsa, previous_version_lsa); !LSA_ISNULL (&process_lsa);) { /* 페이지 fetch + log_get_undo_record 생략 */ if (scan_code == S_DOESNT_FIT && scan_cache->is_recdes_assigned_to_area (*recdes)) { scan_cache->assign_recdes_to_area (*recdes, (size_t) (-recdes->length)); continue; } /* 확장 + 재시도 */ or_mvcc_get_header (recdes, &mvcc_header); snapshot_res = scan_cache->mvcc_snapshot->snapshot_fnc (...); if (snapshot_res == SNAPSHOT_SATISFIED) return MVCC_IS_CHN_UPTODATE (&mvcc_header, has_chn) ? S_SUCCESS_CHN_UPTODATE : S_SUCCESS; else if (snapshot_res == TOO_OLD_FOR_SNAPSHOT) { assert (false); return S_ERROR; } /* <- 불가능: 여기선 더 오래된 버전만 존재 */ else /* TOO_NEW */ LSA_COPY (&process_lsa, &MVCC_GET_PREV_VERSION_LSA (&mvcc_header)); /* 한 단계 더 후퇴 */ }return S_DOESNT_EXIST; /* 체인 소진, 가시 버전 없음 */TOO_OLD는 여기서 불가능하므로(로그에는 더 오래된 버전만 존재) assert로 처리되며; 체인 소진(LSA_ISNULL) → S_DOESNT_EXIST.
5.7 바이트 복사 — heap_get_record_data_when_all_ready와 PEEK vs COPY
섹션 제목: “5.7 바이트 복사 — heap_get_record_data_when_all_ready와 PEEK vs COPY”이 헬퍼는 타입을 spage_get_record 호출에 매핑한다 — ispeeking을 실제로 처리하는 유일한 지점이다:
// heap_get_record_data_when_all_ready -- src/storage/heap_file.cswitch (context->record_type) { case REC_RELOCATION: /* 절대 별칭 금지 -- 강제 COPY */ return spage_get_record (..., fwd_page_watcher.pgptr, forward_oid.slotid, recdes_p, COPY); case REC_BIGONE: return heap_get_bigone_content (..., ispeeking, &forward_oid, recdes_p); case REC_HOME: /* context->ispeeking 준수 */ return spage_get_record (..., home_page_watcher.pgptr, oid_p->slotid, recdes_p, context->ispeeking); default: break; }return S_ERROR;PEEK vs COPY (latch 수명 계약). PEEK는 recdes->data를 latch된 페이지 내부로 반환한다 — 제로 복사이며 latch가 유지되는 동안에만 유효하다; COPY는 사전 할당된 영역으로 memcpy한다. 진입 단언: PEEK, 또는 scan_cache 혹은 호출자가 제공한 recdes->data가 있는 COPY.
불변 조건 (REC_RELOCATION은 절대 peek되지 않음). relocation 케이스는 COPY를 하드코딩한다; context->ispeeking을 사용하면 PEEK 스캔이 포워드 페이지 메모리를 별칭으로 참조하게 되어 heap_clean_get_context가 포워드 watcher를 unfix한 이후 쓰레기 값을 읽게 된다.
5.8 오버플로우 fetch — heap_get_bigone_content
섹션 제목: “5.8 오버플로우 fetch — heap_get_bigone_content”REC_BIGONE 데이터는 오버플로우 파일에 존재하며(### Overflow file); S_DOESNT_FIT 발생 시 영역을 확장한다:
// heap_get_bigone_content -- src/storage/heap_file.cif (scan_cache != NULL && (ispeeking == PEEK || recdes->data == NULL || scan_cache->is_recdes_assigned_to_area (*recdes))) { scan_cache->assign_recdes_to_area (*recdes); while ((scan = heap_ovf_get (thread_p, forward_oid, recdes, NULL_CHN, NULL)) == S_DOESNT_FIT) { assert (recdes->length < 0); scan_cache->assign_recdes_to_area (*recdes, (size_t) (-recdes->length)); } /* 확장 + 재시도 */ if (scan != S_SUCCESS) recdes->data = NULL; /* <- 댕글링 포인터 방지 */ }else scan = heap_ovf_get (thread_p, forward_oid, recdes, NULL_CHN, NULL);스냅샷은 호출자가 이미 검증했으므로 재검사하지 않는다. 재시도 시 -recdes->length(heap_ovf_get이 반환하는 음수 크기)로 재할당한다; 성공이 아닌 경우 recdes->data를 NULL로 설정한다.
5.9 레거시 CHN 패스트 패스 — heap_get_if_diff_chn
섹션 제목: “5.9 레거시 CHN 패스트 패스 — heap_get_if_diff_chn”heap_get_if_diff_chn은 컨텍스트 리팩터링 이전의 원시 함수로, #if defined(ENABLE_UNUSED_FUNCTION) 가드로 보호되어 프로덕션 빌드에 포함되지 않는다. 그 로직 — 헤더만 peek하고 scan == S_SUCCESS_CHN_UPTODATE일 때 데이터 COPY를 건너뜀 — 은 현재 heap_get_visible_version_internal(5.5)의 CHN 단락 경로로 이전되었으며, 이것이 기준 구현이다.
5.10 스캔 변형 — heap_next와 heap_next_internal
섹션 제목: “5.10 스캔 변형 — heap_next와 heap_next_internal”스캔은 페이지 체인을 순회하며, 슬롯을 반복하고, 비객체 슬롯을 필터링하고, 생존한 슬롯을 동일한 가시성 판정 기계(heap_scan_get_visible_version)로 통과시킨다. heap_next, heap_prev, heap_next_sampling, 그리고 *_record_info 변형들은 모두 heap_next_internal 위의 한 줄 래퍼다.
flowchart TD
A[heap_next_internal: hpgid/slot0에서 시작, heap_get_last_vpid, 또는 next_oid에서 재개] --> P[외부 루프: 페이지 단위]
P --> PG{캐시 페이지가 올바른 VPID?}
PG -->|오래됨| SW[old_page_watcher에 stash 후 fetch]
PG -->|없음| FX[heap_scan_pb_lock_and_fetch]
PG -->|히트| IT
SW --> IT
FX --> IT[내부 루프: spage_next_record PEEK]
IT --> T{슬롯 타입}
T -->|slot0 / REC_NEWHOME / REC_ASSIGN_ADDRESS / REC_UNKNOWN| IT
T -->|S_END| NX[heap_vpid_next]
T -->|객체| QV[heap_scan_get_visible_version]
NX -->|NULL_PAGEID| END[S_END 반환]
NX -->|더 있음| P
QV -->|S_SUCCESS, 올바른 클래스| RET[next_oid 설정, S_SUCCESS 반환]
QV -->|S_SUCCESS 잘못된 클래스 / NOT_SATISFIED / DOESNT_EXIST| IT
QV -->|S_ERROR| ERR[오류 반환]
Figure 5-4. heap_next_internal 페이지 체인 + 슬롯 반복.
플로우차트 이외의 내용: latch 유지 stash는 다음 fix가 성공할 때까지 이전 페이지를 old_page_watcher에 고정 상태로 유지한다(스래싱 방지); 이미 캐시된 올바른 페이지는 fix를 생략한다. 필터링은 REC_NEWHOME(자신의 relocation을 거쳐야만 도달), REC_ASSIGN_ADDRESS/REC_UNKNOWN, 그리고 슬롯 HEAP_HEADER_AND_CHAIN_SLOTID를 건너뛴다 — 단, 모든 슬롯이 필요한 get_rec_info 변형(spage_next_record_dont_skip_empty)은 예외다. cache_last_fix_page는 heap_scan_get_visible_version 주변에서 강제로 true로 설정되어 다음 호출을 위해 홈 페이지가 반환되도록 한다; 해당 한정자는 REC_HOME+PEEK 단축 경로를 갖는다(MVCC_IS_HEADER_ALL_VISIBLE 또는 스냅샷 만족 → peek된 recdes, 전체 컨텍스트 불필요).
5.11 페이지 체인 진행 — heap_vpid_next
섹션 제목: “5.11 페이지 체인 진행 — heap_vpid_next”heap_vpid_next는 슬롯 HEAP_HEADER_AND_CHAIN_SLOTID를 읽고 후계 VPID를 반환한다:
// heap_vpid_next -- src/storage/heap_file.cif (spage_get_record (thread_p, pgptr, HEAP_HEADER_AND_CHAIN_SLOTID, &recdes, PEEK) != S_SUCCESS) { VPID_SET_NULL (next_vpid); ret = ER_FAILED; }else { pgbuf_get_vpid (pgptr, next_vpid); if (next_vpid->pageid == hfid->hpgid && next_vpid->volid == hfid->vfid.volid) *next_vpid = ((HEAP_HDR_STATS *) recdes.data)->next_vpid; /* <- 헤더 페이지 */ else *next_vpid = ((HEAP_CHAIN *) recdes.data)->next_vpid; /* <- 일반 페이지 */ }직관적이지 않은 분기 하나: 첫 번째 페이지는 링크를 HEAP_HDR_STATS 내부(체인을 내장)에 저장하고, 다른 모든 페이지는 순수 HEAP_CHAIN에 저장한다 — 현재 VPID를 hfid->hpgid와 비교하여 결정된다(두 구조체는 챕터 1에서 설명). NULL_PAGEID가 순회를 종료시킨다.
5.12 챕터 요약 — 핵심 정리
섹션 제목: “5.12 챕터 요약 — 핵심 정리”- 컨텍스트 하나, 데이터 보유 타입 셋. 데이터를 실제로 담는 것은
REC_HOME/REC_RELOCATION/REC_BIGONE뿐이다; 나머지 모든 타입은S_DOESNT_EXIST(스캔) 또는 오류(포인트 읽기)로 귀결된다. heap_prepare_get_context가 분기 허브다 — 홈을 fix하고, 타입을 읽고, relocation/bigone의 경우 포워드 페이지도 fix한다. 비행 중 홈 unfix 발생 시 단 한 번의try_again재시도가 있으며, 불변 BIGONE 케이스에 대해서는assert(false)가 적용된다.- 포워드 일관성 불변 조건 (
REC_HOME또는 비 null 포워드 OID + 포워드 페이지)은 모든 소비자에서 단언되어,heap_get_mvcc_header와 복사 함수가 포워드 페이지를 맹목적으로 역참조할 수 있게 한다. - 스냅샷 판정은 비대칭이다.
TOO_OLD→S_SNAPSHOT_NOT_SATISFIED;SNAPSHOT_SATISFIED→ 복사;TOO_NEW→prev_version_lsa를 따라 undo 로그로 진입, 만족되거나 소진될 때까지(S_DOESNT_EXIST). - PEEK는 페이지를 별칭으로 참조하고, COPY는 영역에 복제한다 —
REC_RELOCATION은 항상 COPY; CHN 단락 경로는 스냅샷 블록 이후 무조건 실행된다. - 스캔 경로는 포인트 읽기 기계를 재사용한다 —
heap_vpid_next체인 순회, 슬롯 필터링,cache_last_fix_pagelatch 유지를 더하여.heap_get_if_diff_chn은 레거시이며;heap_get_last_version은 업데이터/로커를 위한 스냅샷 없는 형제 함수다.
Chapter 6: 업데이트 흐름과 레코드 타입 전이
섹션 제목: “Chapter 6: 업데이트 흐름과 레코드 타입 전이”힙 업데이트는 힙 매니저에서 가장 분기가 많은 연산이다. 새 이미지는 크기가 줄거나, 동일하게 유지되거나, 늘어날 수 있으며, 원본 슬롯은 REC_HOME이거나, REC_RELOCATION + REC_NEWHOME 쌍이거나, REC_BIGONE 오버플로우일 수 있다. CUBRID의 계약은 OID는 절대 이동하지 않는다 (res_oid를 oid로 재설정)는 것이므로, 크기 증가는 안정적인 홈 슬롯 뒤편의 물리적 표현을 바꾸는 방식으로 흡수된다. 이 챕터는 heap_update_logical이 현재 타입에 따라 어떻게 디스패치하고, 각 워커가 제자리(in-place) / 재배치(relocate) / 오버플로우 중 어느 경로를 선택하는지를 추적한다. 레코드 타입 분류 체계와 MVCC 헤더 레이아웃은 챕터 3을, 삽입 측의 배치 방식은 챕터 4를, 설계 근거는 cubrid-heap-manager.md를 참고한다.
6.1 진입점: 컨텍스트 생성과 MVCC/in-place 분기
섹션 제목: “6.1 진입점: 컨텍스트 생성과 MVCC/in-place 분기”heap_create_update_context는 순수 초기화 함수로, I/O 없이 OID, 클래스 OID, 새 이미지(recdes_p), 그리고 update_in_place 스타일을 기록한다.
// heap_create_update_context -- src/storage/heap_file.cCOPY_OID (&context->oid, oid_p);// ... condensed: COPY_OID (&context->class_oid, ...); context->scan_cache_p = scancache_p; ...context->recdes_p = recdes_p; /* the new image */context->type = HEAP_OPERATION_UPDATE;context->update_in_place = in_place; /* <- in-place vs MVCC switch */UPDATE_INPLACE_STYLE (heap_file.h에 정의)은 세 가지 값을 가진다. UPDATE_INPLACE_NONE은 기본 MVCC 경로로, 새 INSID와 prev_version_lsa 체인 항목을 가진 새 버전을 생성한다. UPDATE_INPLACE_CURRENT_MVCCID는 현재 MVCCID를 스탬핑하는 파괴적 제자리 덮어쓰기로 새 버전을 만들지 않는다. UPDATE_INPLACE_OLD_MVCCID는 기존 MVCC ID를 보존하는 파괴적 덮어쓰기로, 복제(replication)나 재분배(redistribution)에 사용된다. 연산 전체에 적용되는 이 토글은 heap_update_logical 내에서 is_mvcc_op = HEAP_UPDATE_IS_MVCC_OP (is_mvcc_class, update_in_place)로 한 번 계산된다. 여기서 is_mvcc_class = !mvcc_is_mvcc_disabled_class (&class_oid)이며, 매크로는 is_mvcc_class && !HEAP_IS_UPDATE_INPLACE(style)이다.
불변 조건 — MVCC와 in-place는 상호 배타적이다.
is_mvcc_op는 해당 클래스가 MVCC 활성화 상태이고 스타일이UPDATE_INPLACE_NONE일 때만true다.SERVER_MODE가 아닌 빌드에서는HEAP_UPDATE_IS_MVCC_OP가false로 하드코딩되므로, 독립 실행 도구는 항상 in-place 경로를 따른다.
6.2 heap_update_logical: 위치 확인, 페치, 조정, 디스패치
섹션 제목: “6.2 heap_update_logical: 위치 확인, 페치, 조정, 디스패치”스캔 캐시, 파일 타입(FILE_HEAP / FILE_HEAP_REUSE_SLOTS 한정), heap_is_valid_oid 검증 이후, 함수는 홈 페이지를 고정하고(heap_get_record_location), record_type = spage_get_record_type으로 레코드 타입을 읽는다(REC_UNKNOWN 슬롯은 ER_HEAP_UNKNOWN_OBJECT). 이어서 home_recdes에 홈 레코드를 복사하고(언두 소스 — PEEK이 아닌 COPY), 실제 사용자 클래스(!OID_IS_ROOTOID)이면 새 이미지에 heap_update_adjust_recdes_header(6.7절)를 호출하고, do_supplemental_log(CDC)를 결정한 후 디스패치한다.
// heap_update_logical -- src/storage/heap_file.cswitch (context->record_type) { case REC_RELOCATION: rc = heap_update_relocation (thread_p, context, is_mvcc_op); break; case REC_BIGONE: rc = heap_update_bigone (thread_p, context, is_mvcc_op); break; case REC_ASSIGN_ADDRESS: context->is_logical_old = false; /* <- inserted this tran, not an old version */ [[fallthrough]]; case REC_HOME: rc = heap_update_home (thread_p, context, is_mvcc_op); break; default: rc = ER_HEAP_BAD_OBJECT_TYPE; goto exit; }REC_ASSIGN_ADDRESS 폴스루(fallthrough)는 방금 예약된 주소 슬롯을 논리적으로 오래된 버전이 아님으로 표시하여 heap_update_home으로 라우팅한다. exit에서 홈 watcher는 스캔 캐시로 넘겨지거나(cache_last_fix_page인 경우) 고정 해제되며, heap_unfix_watchers가 나머지를 해제한다.
6.3 heap_update_home: REC_HOME이 소스 타입인 경우
섹션 제목: “6.3 heap_update_home: REC_HOME이 소스 타입인 경우”heap_update_home은 REC_HOME(과 REC_ASSIGN_ADDRESS)을 처리하며, 오버플로우 → 제자리 → 재배치의 엄격한 우선순위로 세 가지 목적지 중 하나를 선택한다.
// heap_update_home -- src/storage/heap_file.cif (heap_is_big_length (context->recdes_p->length)) { /* 1. overflow */ heap_ovf_insert (..., &forward_oid, context->recdes_p); heap_build_forwarding_recdes (&forwarding_recdes, REC_BIGONE, &forward_oid); home_page_updated_recdes_p = &forwarding_recdes;}else if (!spage_is_updatable (..., context->recdes_p->length)) { /* 3. relocate */ context->recdes_p->type = REC_NEWHOME; heap_insert_newhome (..., context->recdes_p, &forward_oid, newhome_pg_watcher_p); heap_build_forwarding_recdes (&forwarding_recdes, REC_RELOCATION, &forward_oid); home_page_updated_recdes_p = &forwarding_recdes;}else { /* 2. in place: stays REC_HOME */ context->recdes_p->type = REC_HOME; home_page_updated_recdes_p = context->recdes_p;}이 세 가지는 6.8절 전이 행렬의 행 1~3에 해당한다. 목적지 선택 후, 목적지가 포워더(forwarder)이고 home_page_watcher_p->page_was_unfixed인 경우(정렬된 두 번째 페이지 고정 중 페이지가 해제되어 vacuum/compaction이 발생했을 수 있는 상황) 재-peek 가드가 발동한다. 이 가드는 로깅된 언두 이미지가 현재 바이트와 일치하도록 home_recdes를 다시 읽는다. 그런 다음 로깅(heap_log_update_physical, MVCC인 경우 RVHF_UPDATE_NOTIFY_VACUUM, 그 외에는 RVHF_UPDATE; 살아있는 REC_ASSIGN_ADDRESS도 NOTIFY_VACUUM 사용), prev_version_lsa 캡처, heap_update_physical 호출, MVCC 연산이면 heap_update_set_prev_version 호출이 이어진다.
불변 조건 — REC_ASSIGN_ADDRESS는 MVCC 업데이트 대상이 아니다. 초기 가드(
!HEAP_IS_UPDATE_INPLACE && home_recdes.type == REC_ASSIGN_ADDRESS)는assert(false)와 함께ER_FAILED를 반환한다. 예약(reservation)에는 버전 관리할 MVCC 헤더가 없으므로, 비-MVCC in-place 경로만이 이를 채운다.
flowchart TD
A["heap_update_home"] --> Z{REC_ASSIGN_ADDRESS\n and MVCC op?}
Z -->|yes| ZF["assert false; ER_FAILED"]
Z -->|no| B{heap_is_big_length?}
B -->|yes| C["REC_BIGONE 포워더"]
B -->|no| D{spage_is_updatable home?}
D -->|no| E["REC_RELOCATION + REC_NEWHOME"]
D -->|yes| F["REC_HOME 제자리"]
C --> H["재-peek(unfixed 시); 로그; 물리 갱신; mvcc면 set_prev_version"]
E --> H
F --> H
Figure 6-1: heap_update_home 분기 트리.
6.4 heap_update_relocation: REC_RELOCATION + REC_NEWHOME이 소스 타입인 경우
섹션 제목: “6.4 heap_update_relocation: REC_RELOCATION + REC_NEWHOME이 소스 타입인 경우”세 워커 중 가장 복잡하다. 홈 슬롯은 포워드 페이지의 REC_NEWHOME을 가리키는 REC_RELOCATION OID를 담고 있다. 함수는 forward_oid를 읽고, 포워드 페이지를 고정하며(heap_fix_forward_page), 두 가지 술어를 계산한다.
// heap_update_relocation -- src/storage/heap_file.cfits_in_home = spage_is_updatable (... home slot ..., context->recdes_p->length);fits_in_forward = spage_is_updatable (... forward slot ..., context->recdes_p->length);if (heap_is_big_length (context->recdes_p->length) || (!fits_in_forward && !fits_in_home)) heap_fix_header_page (thread_p, context); /* header page needed for overflow or a new newhome */이어서 4방향 결정이 세 가지 불리언(update_old_home, update_old_forward, remove_old_forward)과 새 홈 이미지를 설정한다 — 6.8절 행 4~7, 그리고 불가능한 else 분기(assert(false); ER_FAILED). fits_in_home이 fits_in_forward보다 우선하지만, 어디에도 맞지 않는 경우가 새 재배치를 생성한 이후에만 그렇다. 임의의 I/O 전에, 두 assert가 부기 불변 조건을 인코딩한다: assert (remove_old_forward != update_old_forward) 및 assert (remove_old_forward == update_old_home).
불변 조건 — 낡은 REC_NEWHOME은 정확히 한 번만 정리된다. 기존
REC_NEWHOME은 삭제되거나(remove_old_forward, 홈 슬롯의 의미가 바뀔 때) 덮어쓰여진다(update_old_forward, 재배치 유지) — 둘 다이거나 둘 다 아닌 경우는 없다. 위의 두 assert는 디버그 빌드에서 위반 시 크래시를 일으킨다. 삭제 XOR 갱신이며, 홈이 재작성될 때만 삭제된다.
이후 최대 세 개의 조건부 블록이 실행된다(홈 재작성은 heap_log_update_physical; 기존 newhome 해제는 heap_log_delete_physical + heap_delete_physical; 또는 제자리 포워드 재작성 로깅 heap_mvcc_log_home_no_change). 재배치 유지(stay-relocated) 브랜치는 prev_version_lsa를 포워드 갱신 LSA로 설정하고, 나머지는 삭제된 기존 newhome의 heap_log_delete_physical 언두 LSA를 사용한다 — 그 레코드 자체가 이전 버전이기 때문이다. MVCC 연산은 이후 newhome_pg_watcher_p(새 newhome) 또는 기존 포워드 watcher와 함께 heap_update_set_prev_version을 호출한다.
6.5 heap_update_bigone: REC_BIGONE이 소스 타입인 경우
섹션 제목: “6.5 heap_update_bigone: REC_BIGONE이 소스 타입인 경우”홈 슬롯은 REC_BIGONE 포워더를 담고 있고, 본문은 오버플로우 파일에 존재한다. ovf_oid는 home_recdes.data에서 읽고, 헤더 페이지를 고정하며, MVCC 연산인 경우 변경 전에 기존 오버플로우 내용을 읽고 RVHF_MVCC_UPDATE_OVERFLOW로 로깅한다.
// heap_update_bigone -- src/storage/heap_file.ccontext->ovf_oid = *((OID *) context->home_recdes.data);heap_fix_header_page (thread_p, context);if (is_mvcc_op) { heap_get_bigone_content (... &context->ovf_oid, &ovf_recdes); /* old version image */ log_append_undo_recdes2 (thread_p, RVHF_MVCC_UPDATE_OVERFLOW, &ovf_vfid, first_pgptr, -1, &ovf_recdes); or_mvcc_set_log_lsa_to_record (context->recdes_p, logtb_find_current_tran_lsa (thread_p)); /* prev_version_lsa */}불변 조건 — 오버플로우 경로는 본문이 변경되기 전에 prev_version을 스탬핑한다.
REC_BIGONE의 경우 이전 버전 LSA는RVHF_MVCC_UPDATE_OVERFLOW언두 레코드 LSA이며, 여기서 새 이미지 헤더에 직접 기록된다.heap_update_set_prev_version(6.7)은 이 워커에서는 호출되지 않는다.
본문 업데이트는 세 가지로 분기한다 — 6.8절 행 8~10:
// heap_update_bigone -- src/storage/heap_file.cif (heap_is_big_length (context->recdes_p->length)) { /* overflow -> overflow */ is_old_home_updated = false; heap_ovf_update (..., &context->ovf_oid, context->recdes_p); if (is_mvcc_op) heap_mvcc_log_home_no_change (...); /* vacuum must still reach new overflow */}else if (spage_update (..., context->recdes_p) == SP_SUCCESS) { /* overflow -> home */ is_old_home_updated = true; context->record_type = context->recdes_p->type = REC_HOME; spage_update_record_type (..., REC_HOME); new_home_recdes = *context->recdes_p;}else { /* overflow -> relocation */ context->recdes_p->type = REC_NEWHOME; heap_insert_newhome (..., context->recdes_p, &newhome_oid, NULL); /* + REC_RELOCATION forwarder */ is_old_home_updated = true;}정리는 is_old_home_updated에 따라 결정된다. 오버플로우 유지(stay-overflow) 브랜치에서는 false(MVCC인 경우 heap_mvcc_log_home_no_change만 수행). 축소 브랜치 둘 다에서는 true이며, 홈 변경 로깅(RVHF_UPDATE_NOTIFY_VACUUM / RVHF_UPDATE)과 heap_ovf_delete로 고아 오버플로우 체인을 회수하는 후속 블록이 실행된다. heap_ovf_update에서 NULL이 반환되면 ASSERT_ERROR_AND_SET; goto exit로 전파된다.
6.6 heap_insert_newhome과 heap_ovf_update — 배치 헬퍼
섹션 제목: “6.6 heap_insert_newhome과 heap_ovf_update — 배치 헬퍼”heap_insert_newhome은 세 워커 모두(그리고 삭제 경로)가 재배치된 REC_NEWHOME을 구체화하는 데 공유한다. 이 함수는 부모의 HFID/클래스를 바탕으로 새 삽입 컨텍스트를 만들고, best-space 검색(챕터 9)으로 레코드를 배치하며, 일반 RVHF_INSERT를 로깅하고(vacuum은 REC_NEWHOME을 스캔하지 않으므로 알리지 않음), OID를 복사해 반환한다.
// heap_insert_newhome -- src/storage/heap_file.cheap_create_insert_context (&ins_context, &parent_context->hfid, &parent_context->class_oid, recdes_p, NULL);heap_find_location_and_insert_rec_newhome (thread_p, &ins_context);heap_log_insert_physical (... ins_context.recdes_p, false, false); /* RVHF_INSERT, not MVCC */if (out_oid_p != NULL) COPY_OID (out_oid_p, &ins_context.res_oid);if (newhome_pg_watcher != NULL) /* hand fixed page back */ pgbuf_replace_watcher (thread_p, ins_context.home_page_watcher_p, newhome_pg_watcher);newhome_pg_watcher가 NULL이 아닌 경우(MVCC 재배치 브랜치), 새로 고정된 페이지는 고정 해제 대신 pgbuf_replace_watcher로 반환되어, heap_update_set_prev_version이 나중에 이전 버전 필드를 제자리에서 패치할 수 있도록 한다. heap_ovf_update는 오버플로우 VFID를 해석하고(heap_ovf_find_vfid) overflow_update에 위임하는 얇은 래퍼로, ovf_oid 또는 NULL을 반환한다.
6.7 헤더 스탬핑: heap_update_adjust_recdes_header와 heap_update_set_prev_version
섹션 제목: “6.7 헤더 스탬핑: heap_update_adjust_recdes_header와 heap_update_set_prev_version”이 두 함수는 MVCC 버전 체인 작업을 연산의 타임라인에 걸쳐 분담한다. heap_update_adjust_recdes_header는 앞서(heap_update_logical에서) 실행되어, 새 이미지의 헤더를 NULL prev-version LSA로 덮어쓴다(언두 LSA는 로깅 전까지 알 수 없다). 최적화 경로(MVCC 연산, 이미지가 크지 않음, 소스 헤더에 DELID 없음)는 하나의 memmove로 INSID + prev-version LSA를 위한 공간을 확보하고, 새 MVCCID를 스탬핑하며, 플레이스홀더 NULL LSA를 기록한다.
// heap_update_adjust_recdes_header -- src/storage/heap_file.cupdate_mvcc_flags = OR_MVCC_FLAG_VALID_INSID | OR_MVCC_FLAG_VALID_PREV_VERSION;mvcc_id = logtb_get_current_mvccid (thread_p);if ((mvcc_flags & update_mvcc_flags) != update_mvcc_flags) { repid_and_flag_bits |= (update_mvcc_flags << OR_MVCC_FLAG_SHIFT_BITS); memmove (new_data_p, existing_data_p, ...); /* room for INSID + LSA; ... condensed ... */}OR_PUT_BIGINT (new_ins_mvccid_pos_p, &mvcc_id); /* fresh INSID */memcpy (new_ins_mvccid_pos_p + OR_MVCCID_SIZE, &null_lsa, ...); /* placeholder LSA */느린 경로(or_mvcc_get_header + 플래그별 편집)는 나머지 케이스를 처리한다. UPDATE_INPLACE_OLD_MVCCID는 기존 INSID를 유지하고, 비-MVCC 경로는 모든 MVCC 플래그를 제거하며(MVCC_CLEAR_ALL_FLAG_BITS), 큰 레코드는 OR_MVCC_MAX_HEADER_SIZE를 강제한다. 모든 MVCC 경로는 prev_version_lsa를 NULL로 남긴다.
heap_update_set_prev_version은 물리적 업데이트와 로깅 이후에 실행되어, PEEK으로 이제 알려진 언두 LSA를 레코드에 패치한다(spage_update 없이). 현재 홈 슬롯 타입에 따라 디스패치한다.
// heap_update_set_prev_version -- src/storage/heap_file.cspage_get_record (... home_pg_watcher->pgptr, oid->slotid, &recdes, PEEK);if (recdes.type == REC_HOME) { /* patch home record */ or_mvcc_set_log_lsa_to_record (&recdes, prev_version_lsa);} else if (recdes.type == REC_RELOCATION) { /* follow to REC_NEWHOME, patch it */ forward_oid = *((OID *) recdes.data); spage_get_record (... fwd_pg_watcher->pgptr, forward_oid.slotid, &forward_recdes, PEEK); or_mvcc_set_log_lsa_to_record (&forward_recdes, prev_version_lsa);} else if (recdes.type == REC_BIGONE) { /* patch overflow OR header */ forward_recdes.data = overflow_get_first_page_data (overflow_pg_watcher.pgptr); or_mvcc_set_log_lsa_to_record (&forward_recdes, prev_version_lsa);} else { assert (false); error_code = ER_FAILED; } /* each arm pgbuf_set_dirty's its page */불변 조건 — prev_version_lsa는 직전 버전의 언두 이미지를 가리킨다.
heap_update_adjust_recdes_header가 슬롯을 예약하고,heap_update_set_prev_version(홈/재배치 경우) 또는heap_update_bigone의 인라인 스탬핑이 로깅 시점 LSA로 채운다.REC_RELOCATION경로는 포워드 페이지에fwd_pg_watcher가 필요하므로, 6.6절의pgbuf_replace_watcher핸드오프가 그 이유다.
6.8 소스 타입 × 새 크기 전이 행렬
섹션 제목: “6.8 소스 타입 × 새 크기 전이 행렬”세 워커를 합쳐서 도달 가능한 모든 전이를 정리하면 다음과 같다.
| 소스 타입 | 크기 조건 | 결과 홈 타입 | 본문 처리 |
|---|---|---|---|
REC_HOME | big | REC_BIGONE | 새 오버플로우 삽입 |
REC_HOME | not big, 홈 공간 부족 | REC_RELOCATION | 새 REC_NEWHOME |
REC_HOME | not big, 홈 공간 충분 | REC_HOME | 제자리 갱신 |
REC_RELOCATION | big | REC_BIGONE | 새 오버플로우; 기존 newhome 삭제 |
REC_RELOCATION | not big, 둘 다 불가 | REC_RELOCATION | 새 newhome; 기존 newhome 삭제 |
REC_RELOCATION | not big, 홈 공간 충분 | REC_HOME | 이미지를 홈에; 기존 newhome 삭제 |
REC_RELOCATION | not big, 포워드 공간 충분 | REC_RELOCATION (유지) | 기존 newhome 제자리 갱신 |
REC_BIGONE | big | REC_BIGONE (유지) | heap_ovf_update 제자리 |
REC_BIGONE | not big, spage_update 성공 | REC_HOME | 오버플로우 삭제 |
REC_BIGONE | not big, spage_update 실패 | REC_RELOCATION | 새 newhome; 오버플로우 삭제 |
세 소스 전체에 걸친 통합 우선순위는 오버플로우 > 홈 > 재배치이며, heap_update_relocation만의 특수화로 이미 고정된 포워드 슬롯을 재사용하는 것(재배치 유지)이 새 newhome 할당보다 우선한다.
6.9 챕터 요약 — 핵심 정리
섹션 제목: “6.9 챕터 요약 — 핵심 정리”heap_update_logical은 현재 홈 슬롯 타입에 따라 세 워커 중 하나로 디스패치한다. OID는 절대 이동하지 않으며(res_oid = oid), 오직 그 뒤편의 물리적 표현만 바뀐다.- MVCC 대 in-place 분기는 하나의 불리언(
is_mvcc_op = is_mvcc_class && update_in_place == UPDATE_INPLACE_NONE)으로 결정되며, 상호 배타적임이 assert로 보장된다. in-place 스타일은 현재 또는 기존 MVCCID를 스탬핑한다. - 각 워커는 오버플로우 > 홈 > 재배치 우선순위로 목적지를 선택한다.
heap_update_relocation은 이미 고정된 포워드 슬롯을 선호하는 재배치 유지 특수화를 추가한다. heap_update_relocation의 세 불리언은 낡은REC_NEWHOME을 정확히 한 번 정리함을 보장하는 두 assert로 지켜진다 — 홈의 의미가 바뀔 때 삭제, 재배치가 유지될 때 갱신, 둘 다이거나 둘 다 아닌 경우는 없다.- 헤더 스탬핑은 시간적으로 분리된다.
heap_update_adjust_recdes_header가 앞서 INSID + NULL prev-version LSA를 위한 공간을 확보하고, LSA는 나중에(heap_update_set_prev_version으로, 또는 bigone의RVHF_MVCC_UPDATE_OVERFLOW인라인으로) 패치되어, 챕터 5의 읽기 경로 버전 체인이 유지된다.
Chapter 7: 삭제 흐름과 툼스톤(Tombstone)
섹션 제목: “Chapter 7: 삭제 흐름과 툼스톤(Tombstone)”CUBRID에서 DELETE는 “바이트를 지우는” 연산이 아니다. MVCC 하에서는 삭제가 커밋되기 전에 시작된 스냅샷이 여전히 이전 행을 볼 수 있어야 하므로 레코드 본체가 살아있어야 한다. 따라서 MVCC 삭제는
Chapter 6의 제자리 업데이트와 거의 동일하다 — 헤더에 삭제 MVCCID를 찍고 페이로드는 그대로 둔다. spage_delete를 호출하는 물리적 삭제는 비-MVCC 테이블이나 포워딩된 본체를 최종적으로 해제할 때에만 발생한다. 이 챕터는 세 가지 질문에 답한다: DELETE가 독자를 위해 레코드를 어떻게 제자리에 남기는가, 어느 지점에서 UPDATE와 갈라지는가, 그리고 슬롯이 실제로 제거되는 경우는 언제인가? (찍힌 mvcc_del_id를 다시 읽는 경로는 Chapter 5에서 다룬다.)
7.1 진입점: heap_create_delete_context와 삭제 디스패처
섹션 제목: “7.1 진입점: heap_create_delete_context와 삭제 디스패처”DELETE는 heap_create_delete_context로 HEAP_OPERATION_CONTEXT를 구성하여 진입한다. 업데이트 컨텍스트 빌더(Chapter 6)와 달리 의도적으로 단출하다 — recdes_p가 없다: 새 레코드는 이미 페이지에 있는 레코드로부터 삭제 도중에 합성되므로, 호출자는 OID만 넘기면 된다.
// heap_create_delete_context -- src/storage/heap_file.cheap_clear_operation_context (context, hfid_p);COPY_OID (&context->oid, oid_p); COPY_OID (&context->class_oid, class_oid_p);context->scan_cache_p = scancache_p;context->type = HEAP_OPERATION_DELETE; /* <- no recdes_p */heap_delete_logical이 Figure 7-1에 나타난 분기를 주도한다: 입력 유효성 검사(heap_is_valid_oid / heap_scancache_check_with_hfid, 페이지를 건드리기 전에 ER_FAILED로 실패); 파일 타입 검사(FILE_HEAP/FILE_HEAP_REUSE_SLOTS가 아니면 치명적 오류); 루트 클래스 처리(heap_mark_class_as_modified); MVCC 결정(아래 불변식 참조); X_LOCK 하에서 heap_get_record_location + spage_get_record_type으로 위치 결정(REC_UNKNOWN 슬롯은 ER_HEAP_UNKNOWN_OBJECT 발생); COPY 스냅샷으로 context->home_recdes 채우기; 마지막으로 record_type에 따른 분기:
// heap_delete_logical -- src/storage/heap_file.c#if defined (SERVER_MODE) if (mvcc_is_mvcc_disabled_class (&context->class_oid)) is_mvcc_op = false; /* <- catalog/system class */ else is_mvcc_op = true; /* <- ordinary user table */#else is_mvcc_op = false; /* <- standalone (SA) mode */#endif // ... condensed ... switch (context->record_type) { case REC_BIGONE: rc = heap_delete_bigone (thread_p, context, is_mvcc_op); break; case REC_RELOCATION: rc = heap_delete_relocation (thread_p, context, is_mvcc_op); break; case REC_HOME: case REC_ASSIGN_ADDRESS: rc = heap_delete_home (thread_p, context, is_mvcc_op); break; default: /* REC_NEWHOME, REC_MARKDELETED, ... reached directly => bug */ er_set (..., ER_HEAP_BAD_OBJECT_TYPE, ...); rc = ER_FAILED; goto error; }flowchart TD
A["heap_delete_logical"] --> B{"유효한 oid &\nFILE_HEAP?"}
B -- no --> ERR["ER_FAILED / 치명적 오류"]
B -- yes --> D{"mvcc 비활성화 클래스\n또는 SA 모드?"}
D -- yes --> E["is_mvcc_op=false"]
D -- no --> F["is_mvcc_op=true"]
E --> H{"record_type"}
F --> H
H -- REC_HOME / REC_ASSIGN_ADDRESS --> I["heap_delete_home"]
H -- REC_RELOCATION --> J["heap_delete_relocation"]
H -- REC_BIGONE --> K["heap_delete_bigone"]
H -- other --> L["ER_HEAP_BAD_OBJECT_TYPE"]
Figure 7-1 — heap_delete_logical 분기 맵 (rootclass + X_LOCK 위치 결정은 생략). 업데이트와 삭제는 동일한 세 개의 워커를 공유한다.
불변식 7-A — MVCC/물리적 삭제 결정은 클래스로부터 한 번만 이루어지며, 변경 없이 아래로 전달된다.
is_mvcc_op는 클래스 OID를!mvcc_is_mvcc_disabled_class에 적용하여 한 번만 계산되고(standalone은false) 모든 워커에 그대로 전달된다. 이 값이 잘못되면 사용자 행이 물리적으로 삭제되어 여전히 보여야 할 스냅샷에서 사라진다.
7.2 heap_delete_home — REC_HOME / REC_ASSIGN_ADDRESS 경로
섹션 제목: “7.2 heap_delete_home — REC_HOME / REC_ASSIGN_ADDRESS 경로”heap_delete_home은 업데이트 워커 heap_update_home(Chapter 6)의 골격을 공유한다 — 플래그 읽기, 빠른/느린 경로를 통한 빌드, 분류, 크기 초과 시 재배치 — 하지만 다음 지점에서 갈라진다:
| 항목 | UPDATE 워커 (Ch 6) | DELETE 워커 (이 챕터) |
|---|---|---|
| 새 페이로드 | 호출자의 context->recdes_p | 기존 레코드로부터 합성됨 |
| 헤더 스탬프 | 새 repid/데이터, prev_version_lsa 재기록 | mvcc_del_id 설정; prev_version_lsa 미기록 |
| 크기 변화 | 임의적 (많이 줄거나 늘 수 있음) | DELID가 없었을 때 최대 OR_MVCCID_SIZE(8바이트) 증가 |
| 본체 바이트 | 교체됨 | 그대로 복사 |
| 포워더/오버플로 | 생성되거나 해제될 수 있음 | 반드시 필요한 경우에만 접근 (REC_BIGONE: 제자리 편집) |
불변식 7-B — MVCC 삭제는
prev_version_lsa를 기록하지 않는다. 삭제는 새 버전을 만들지 않으므로heap_delete_adjust_header는OR_MVCC_FLAG_VALID_DELID와 DELID만 설정한다. 빠른 경로는delid_offset이후 바이트를 복사하여 기존 prev-version 바이트를 보존한다. 이를 어기면 이후 버전에서 체인을 따라가는 독자가 죽은 레코드로 되돌아가는 루프에 빠진다.
페이지가 언픽스된 경우 레코드를 다시 가져온 후(vacuum이 축소시켰을 수 있는 경우를 대비한 가드), heap_delete_home은 is_mvcc_op로 분기한다. MVCC 분기는 use_optimization으로 제어되는 두 경로 중 하나로 삭제 스탬프가 찍힌 레코드를 만든다:
// heap_delete_home -- src/storage/heap_file.crepid_and_flag_bits = OR_GET_MVCC_REPID_AND_FLAG (context->home_recdes.data);mvcc_flags = (repid_and_flag_bits >> OR_MVCC_FLAG_SHIFT_BITS) & OR_MVCC_FLAG_MASK;adjusted_size = context->home_recdes.length;use_optimization = true;if (!(mvcc_flags & OR_MVCC_FLAG_VALID_DELID)) { adjusted_size += OR_MVCCID_SIZE; /* <- 8 more bytes for DELID */ is_adjusted_size_big = heap_is_big_length (adjusted_size); if (is_adjusted_size_big) use_optimization = false; /* rare: spills to overflow */ }else { /* DELID already set: re-delete of vacuum-pending row */ is_adjusted_size_big = false; use_optimization = false; }빠른 경로 (use_optimization; DELID 없음, 결과 크기 작음)는 순수한 바이트 수술이다 — [0, delid_offset) 구간을 복사하고, 선두 워드에 OR_MVCC_FLAG_VALID_DELID를 OR 연산으로 설정하고, 8바이트 MVCCID를 끼워 넣고, 나머지를 복사한다 — 헤더 파싱 없음. 느린 경로 (DELID 이미 있거나 레코드가 big 크기가 될 경우)는 or_mvcc_get_header, heap_delete_adjust_header, or_mvcc_add_header로 재직렬화한 뒤 memcpy로 본체를 붙인다.
// heap_delete_adjust_header -- src/storage/heap_file.cMVCC_SET_FLAG_BITS (header_p, OR_MVCC_FLAG_VALID_DELID);MVCC_SET_DELID (header_p, mvcc_id); /* <- the death stamp, nothing else */if (need_mvcc_header_max_size) HEAP_MVCC_SET_HEADER_MAXIMUM_SIZE (header_p); /* <- only when spilling to REC_BIGONE */만들어진 레코드는 분류된다 — is_adjusted_size_big이면 REC_BIGONE, !spage_is_updatable(... built_recdes.length)이면 REC_NEWHOME, 그 외에는 REC_HOME — 그리고 처리된다(Figure 7-2). REC_HOME(일반적인 경우)은 재배치 없이 heap_mvcc_log_delete(..., RVHF_MVCC_DELETE_REC_HOME)를 로깅한 뒤 heap_update_physical로 슬롯을 제자리에서 덮어쓴다. OID와 본체는 변하지 않는다. REC_NEWHOME(슬롯에 더 이상 맞지 않는 경우)은 heap_insert_newhome을 호출하고, heap_build_forwarding_recdes로 REC_RELOCATION 포워더를 만들고, heap_mvcc_log_home_change_on_delete를 로깅한 뒤 heap_update_physical로 홈을 기록한다. REC_BIGONE(big-length 임계값을 넘는 경우)은 오버플로에 heap_ovf_insert를 사용한다는 점만 다르고 동일하다.
비-MVCC 분기 (is_mvcc_op == false): 헤더 조작 없이 is_reusable = heap_is_reusable_oid(context->file_type)을 구한 뒤 heap_log_delete_physical(..., is_reusable, ...) + heap_delete_physical을 호출한다. 슬롯 재사용 여부는 플래그가 결정하며, 페이지가 아니다(§7.5).
flowchart TD
A["heap_delete_home"] --> B{"is_mvcc_op?"}
B -- no --> P["로그 + heap_delete_physical"]
B -- yes --> C{"DELID 존재\n또는 크기 big?"}
C -- no, small --> D["빠른 경로: DELID 플래그 + MVCCID 설정"]
C -- yes/rare --> E["느린 경로: get/adjust/add header"]
D --> F{"분류"}
E --> F
F -- REC_HOME --> G["RVHF_MVCC_DELETE_REC_HOME\n제자리 업데이트"]
F -- REC_NEWHOME --> H["heap_insert_newhome\n홈에 REC_RELOCATION 기록"]
F -- REC_BIGONE --> I["heap_ovf_insert\n홈에 REC_BIGONE 기록"]
Figure 7-2 — heap_delete_home. MVCC 분기에서 8바이트 DELID가 슬롯을 초과하면 홈 레코드가 relocated/big으로 승격될 수 있다.
7.3 heap_delete_relocation — 이미 포워딩된 경로
섹션 제목: “7.3 heap_delete_relocation — 이미 포워딩된 경로”홈 슬롯이 REC_RELOCATION 포워더인 경우, 본체는 포워드 페이지의 REC_NEWHOME 슬롯에 있다. heap_delete_relocation은 포워드 페이지를 픽스하고(heap_fix_forward_page), 포워드 레코드를 peek하고, (MVCC 분기에서) §7.2의 빠른/느린 경로로 삭제 스탬프가 찍힌 레코드를 만든다. 세 개의 불리언이 어디에 저장될지를 결정한다:
| 분기 | remove_old_forward | update_old_forward | update_old_home | 새 홈 레코드 |
|---|---|---|---|---|
is_adjusted_size_big | yes | no | yes | REC_BIGONE 포워더 |
fits_in_home | yes | no | yes | REC_HOME (본체가 홈으로 접힘) |
fits_in_forward | no | yes | no | 변경 없음 (포워드 제자리 업데이트) |
| else | yes | no | yes | 새 NEWHOME으로 REC_RELOCATION |
후처리: update_old_home은 홈 슬롯을 재기록한다(heap_mvcc_log_home_change_on_delete + heap_update_physical); else/fits_in_forward 경우는 vacuum 상태를 진행시키기 위해 heap_mvcc_log_home_no_change를 로깅한다; update_old_forward는 포워드 슬롯을 제자리에서 재기록한다(heap_mvcc_log_delete(..., RVHF_MVCC_DELETE_REC_NEWHOME)); remove_old_forward는 기존 포워드 슬롯을 해제하고(log_append_undoredo_recdes(RVHF_DELETE, ...) + heap_delete_physical), 재배치된 본체는 인덱스에서 참조되지 않으므로 재사용 가능한 OID 힙에는 RVHF_MARK_REUSABLE_SLOT postpone을 추가한다.
비-MVCC 분기: 두 슬롯을 모두 물리적으로 삭제한다(홈 먼저, 그 다음 포워드, 각각 heap_log_delete_physical + heap_delete_physical). 포워드 슬롯은 힙 타입에 관계없이 항상 mark_reusable = true로 로깅된다 — 재배치된 레코드는 인덱스에서 직접 참조되지 않기 때문이다.
불변식 7-C — 홈 슬롯을 재기록하지 않는 모든 MVCC 삭제에서
heap_mvcc_log_home_no_change가 실행된다. Vacuum은 페이지의 vacuum 체인(Chapter 8)을 최대 MVCCID를 기준으로 순회한다. 포워드/오버플로 페이지만 건드리는 삭제는 홈 체인을 갱신하지 않아 vacuum이 다시 방문하지 않게 된다. 이 호출은heap_page_update_chain_after_mvcc_op로 체인을 업데이트하고, 상태 전환이 발생하면 로그 오프셋에HEAP_RV_FLAG_VACUUM_STATUS_CHANGE를 OR 연산으로 설정하여 복구 시 재구성이 가능하게 한다.
7.4 heap_delete_bigone — 고정 크기 오버플로 헤더 편집
섹션 제목: “7.4 heap_delete_bigone — 고정 크기 오버플로 헤더 편집”REC_BIGONE 홈 슬롯에는 오버플로 OID만 있고, 실제 레코드(전체 크기 MVCC 헤더 포함)는 오버플로 페이지에 있다. 본체를 이동하는 대신, MVCC 분기는 오버플로 페이지의 헤더를 제자리에서 편집한다:
// heap_delete_bigone -- src/storage/heap_file.coverflow_oid = *((OID *) context->home_recdes.data); /* home holds only the OID */// ... fix overflow page WRITE, check PAGE_OVERFLOW ptype ...heap_get_mvcc_rec_header_from_overflow (... &overflow_header ...);heap_mvcc_log_delete (thread_p, &log_addr, RVHF_MVCC_DELETE_OVERFLOW);heap_delete_adjust_header (&overflow_header, mvcc_id, false); /* <- false: already max size */rc = heap_set_mvcc_rec_header_on_overflow (context->overflow_page_watcher_p->pgptr, &overflow_header);heap_set_mvcc_rec_header_on_overflow는 전용 변경자(mutator)다. 오버플로 헤더는 항상 최대 크기로 저장되어 있으므로(모든 플래그 존재, 슬롯 미리 예약), DELID를 쓸 때 페이로드 바이트를 하나도 이동시키지 않는다:
// heap_set_mvcc_rec_header_on_overflow -- src/storage/heap_file.covf_recdes.data = overflow_get_first_page_data (ovf_page);ovf_recdes.area_size = ovf_recdes.length = OR_HEADER_SIZE (ovf_recdes.data);// force INSID slot present (MVCCID_ALL_VISIBLE) if absent ...if (!MVCC_IS_FLAG_SET (mvcc_header, OR_MVCC_FLAG_VALID_DELID)) { /* force DELID slot present */ MVCC_SET_FLAG_BITS (mvcc_header, OR_MVCC_FLAG_VALID_DELID); MVCC_SET_DELID (mvcc_header, MVCCID_NULL); }return or_mvcc_set_header (&ovf_recdes, mvcc_header); /* <- overwrites only the header region */오버플로에 스탬프를 찍은 후 홈 슬롯은 재기록되지 않지만, heap_mvcc_log_home_no_change는 여전히 홈 슬롯을 로깅하여 홈 페이지의 vacuum 체인이 진행된다(불변식 7-C). 포워더는 여전히 오버플로 레코드를 가리키며, 비-MVCC 분기에서만 해제된다.
비-MVCC 분기: 두 곳 모두 해제한다 — heap_log_delete_physical + heap_delete_physical로 홈 포워더 슬롯을 제거하고, heap_ovf_delete로 오버플로 체인을 해제한다(이를 호출하는 유일한 삭제 경로).
불변식 7-D — 오버플로 헤더는
OR_MVCC_MAX_HEADER_SIZE로 고정되어 있으므로REC_BIGONE삭제는 재배치가 발생하지 않는다. 컴팩트한 홈/재배치 헤더는 8바이트 DELID가 추가될 때 슬롯/big-length 경계를 넘을 수 있다(§7.2, §7.3). 반면 오버플로 헤더는 항상 모든 슬롯을 포함하므로(변경자가 INSID와 DELID를 강제로 존재하게 함) DELID는 미리 예약된 공간에 쓰인다 — 따라서need_mvcc_header_max_size는false다. 컴팩트하게 저장된 경우 제자리 스탬프는 본체를 덮어쓰게 된다.
7.5 heap_delete_physical, spage_delete, 그리고 OID 재사용 결정 방식
섹션 제목: “7.5 heap_delete_physical, spage_delete, 그리고 OID 재사용 결정 방식”모든 물리적 삭제는 heap_delete_physical을 통한다. 이는 얇은 래퍼로서 spage_get_free_space_without_saving으로 스냅샷을 찍고, spage_delete를 호출하고(NULL_SLOTID이면 ER_FAILED), heap_stats_update(최적 공간 캐시, Ch 9)와 pgbuf_set_dirty를 처리한다. spage_delete는 페이지 헤더의 anchor_type에 따라 툼스톤 형태를 결정한다:
// spage_delete -- src/storage/slotted_page.cswitch (page_header_p->anchor_type) /* ANCHORED arms set offset_to_record = SPAGE_EMPTY_OFFSET */ { case ANCHORED: /* non-heap callers only */ slot_p->record_type = REC_DELETED_WILL_REUSE; break; case ANCHORED_DONT_REUSE_SLOTS: /* <- ALWAYS taken by heap deletes */ slot_p->record_type = REC_MARKDELETED; break; // UNANCHORED_* (not used by heap files) remove the slot entry; default asserts }anchor 타입만으로는 힙을 구별할 수 없다: 모든 힙 페이지는 ANCHORED_DONT_REUSE_SLOTS다. heap_get_spage_type이 (FILE_HEAP과 FILE_HEAP_REUSE_SLOTS 양쪽 모두) 무조건 해당 타입을 반환하고, 모든 힙의 spage_initialize가 이 타입을 전달한다. 따라서 힙 삭제는 항상 해당 분기로 가서 REC_MARKDELETED를 찍는다 — 본체 바이트는 회수되지만 SPAGE_SLOT 엔트리는 유지된다. ANCHORED 분기는 비-힙 전용이다.
OID 재사용 여부는 호출자가 결정한다: FILE_HEAP_REUSE_SLOTS 테이블의 경우 heap_is_reusable_oid(file_type)이 true이므로, 워커는 물리적 삭제 후 RVHF_MARK_REUSABLE_SLOT postpone을 추가하고(heap_log_delete_physical에서 mark_reusable일 때, 그리고 재배치 포워드 슬롯 경로에서), redo 핸들러 heap_rv_redo_mark_reusable_slot이 커밋 시와 복구 재생 시에 업그레이더를 실행한다:
// spage_mark_deleted_slot_as_reusable -- src/storage/slotted_page.c// ... asserts slot is empty and REC_MARKDELETED/REC_DELETED_WILL_REUSE ...slot_p->record_type = REC_DELETED_WILL_REUSE; /* <- REC_MARKDELETED -> reusable */불변식 7-E — 재사용 가능성은 페이지 anchor 타입이 아닌 호출자(
heap_is_reusable_oid+RVHF_MARK_REUSABLE_SLOTpostpone)가 결정한다. 모든 힙 페이지는ANCHORED_DONT_REUSE_SLOTS이므로spage_delete는 항상REC_MARKDELETED로 툼스톤을 찍는다.FILE_HEAP_REUSE_SLOTS삭제만(heap_is_reusable_oid로 게이팅) postpone을 추가하며, 그 핸들러가spage_mark_deleted_slot_as_reusable을 실행해REC_MARKDELETED -> REC_DELETED_WILL_REUSE로 업그레이드한다. 재사용 불가능한 힙이 이 postpone을 추가하면 그 OID가 낡은 인덱스 키 아래에서 재활용된다. 방증:spage_delete_for_recovery는ANCHORED_DONT_REUSE_SLOTS만 특수 처리한다(인서트를 undo하는 동안 새REC_MARKDELETED를REC_DELETED_WILL_REUSE로 다운그레이드 — 커밋된 적 없는 레코드는 외부 OID 참조가 없다) — 힙 페이지가 해당 타입이 아니라면 무의미한 처리다.
7.6 챕터 요약 — 핵심 정리
섹션 제목: “7.6 챕터 요약 — 핵심 정리”- DELETE는 업데이트 워커를 재사용한다 —
heap_delete_logical은heap_delete_home/heap_delete_relocation/heap_delete_bigone으로 분기하는데, 이는 Chapter 6의 업데이트와 정확히 동일하다. 차이는 워커 내부에 있으며, 구조에 있지 않다. - MVCC 삭제 = 제거가 아닌 스탬프 — 워커는
heap_delete_adjust_header로OR_MVCC_FLAG_VALID_DELID와 MVCCID를 설정하고 제자리에 기록한다. 본체는 살아남고prev_version_lsa는 기록되지 않는다(불변식 7-B). 8바이트 스탬프가 컴팩트한REC_HOME을REC_NEWHOME이나REC_BIGONE으로 밀어낼 수 있으므로, 워커는 빌드 후 재분류한다. REC_BIGONE은 저렴한 경우다 — 오버플로 헤더는OR_MVCC_MAX_HEADER_SIZE로 저장되므로heap_set_mvcc_rec_header_on_overflow가 본체를 이동하지 않고 미리 예약된 공간에 DELID를 찍는다(불변식 7-D).- Vacuum은 항상 통보된다 — 홈 슬롯이 변경되지 않는 경우에도
heap_mvcc_log_home_no_change가 vacuum 체인을 진행시킨다(불변식 7-C). 물리적 삭제는 비-MVCC 경로(카탈로그, standalone)와 포워딩/오버플로 본체를 해제할 때에만 발생한다. - 페이지 anchor 타입은 OID 재사용을 결정하지 않는다 (불변식 7-E) — 모든 힙 페이지는
ANCHORED_DONT_REUSE_SLOTS(heap_get_spage_type)이므로spage_delete는 항상REC_MARKDELETED를 찍는다.FILE_HEAP_REUSE_SLOTS삭제(heap_is_reusable_oid로 게이팅)만RVHF_MARK_REUSABLE_SLOTpostpone을 추가하며, 그 핸들러가spage_mark_deleted_slot_as_reusable을 실행해REC_MARKDELETED -> REC_DELETED_WILL_REUSE로 업그레이드한다.
Chapter 8: Vacuum 회수와 페이지 Vacuum 상태
섹션 제목: “Chapter 8: Vacuum 회수와 페이지 Vacuum 상태”6장과 7장은 죽은 튜플을 페이지에 그대로 남겨뒀다. 삭제된 레코드의
mvcc_del_id, prev_version_lsa 체인 끝의 superseded 버전이 그것이다.
당시에는 더 오래된 스냅샷이 이전 버전을 여전히 필요로 할 수 있었기 때문에
즉각 해제하지 않았다. 그 지연된 회수자가 vacuum이다. 이 챕터는
“누가 물리적으로 slot을 회수하는가, 그리고 페이지는 어떻게 해제해도 안전하다고
판단하는가?”라는 물음에 답한다. MVCC 가시성 이론과 active/vacuum 분리에 대해서는
cubrid-heap-manager.md의 “Vacuum and the dead-version problem” 절을 참고하라.
여기서는 코드 흐름을 추적한다.
8.1 임계값 계약과 페이지 순회 드라이버
섹션 제목: “8.1 임계값 계약과 페이지 순회 드라이버”Vacuum은 개별 스냅샷을 직접 따지지 않는다. 전달받는 것은 단 하나,
threshold_mvccid — 아직 활성 트랜잭션에 가시적일 수 있는 가장 오래된
MVCCID다. 이 임계값보다 앞선 mvcc_del_id(삭제) 또는 insert MVCCID(superseded
버전)를 가진 버전은 누구에게도 보이지 않으므로 파기할 수 있다.
heap_vacuum_all_objects는 대량 진입점(compactdb, 클래스 purge)으로,
파일을 페이지 단위로 순회한다.
// heap_vacuum_all_objects -- src/storage/heap_file.c next_vpid.pageid = upd_scancache->node.hfid.hpgid; /* <- start at header page */ reusable = heap_is_reusable_oid (upd_scancache->file_type); /* <- slot reuse policy */ while (!VPID_ISNULL (&next_vpid)) { vpid = next_vpid; error_code = pgbuf_ordered_fix (thread_p, &vpid, OLD_PAGE, PGBUF_LATCH_WRITE, &pg_watcher); if (error_code != NO_ERROR) { goto exit; } /* <- fix failed */ // ... unfix old_pg_watcher (previous page in the rolling double-watcher) ... error_code = heap_vpid_next (thread_p, &upd_scancache->node.hfid, pg_watcher.pgptr, &next_vpid); if (error_code != NO_ERROR) { assert (false); goto exit; } /* <- corrupt next_vpid chain */ worker.n_heap_objects = spage_number_of_slots (pg_watcher.pgptr) - 1; /* <- minus header slot */ if (worker.n_heap_objects > 0 && heap_page_get_vacuum_status (thread_p, pg_watcher.pgptr) != HEAP_PAGE_VACUUM_NONE) { // ... fill worker.heap_objects[i].oid.slotid = 1..n; skip already-clean pages ... error_code = vacuum_heap_page (thread_p, worker.heap_objects, worker.n_heap_objects, threshold_mvccid, &upd_scancache->node.hfid, &reusable, false); if (error_code != NO_ERROR) { goto exit; } } pgbuf_replace_watcher (thread_p, &pg_watcher, &old_pg_watcher); }exit: // ... unfix both watchers, free worker.heap_objects ...오류 분기는 인라인으로 주석 처리했다(모든 분기는 Figure 8-1에서 확인 가능).
설계의 핵심은 HEAP_PAGE_VACUUM_NONE 건너뜀 분기다 — vacuum이 필요 없는
페이지는 스캔 자체를 하지 않는다. 나머지 경우에는 드라이버가 OID 배열
[1..n]을 구성한 뒤 vacuum_heap_page(query/vacuum.c)에 위임한다.
spage_vacuum_slot, heap_page_set_vacuum_status_none,
heap_remove_page_on_vacuum을 유일하게 호출하는 곳이 바로 이 함수다:
heap_vacuum_all_objects는 후보 페이지를 선택하고,
vacuum_heap_page는 후보 slot을 선택한다.
불변 조건 — vacuum은 반드시 죽었다고 증명된 버전만 해제한다. 하나의 slot이
spage_vacuum_slot에 도달하려면vacuum_heap_record가vacuum_is_mvccid_vacuumed로 해당 MVCCID가threshold_mvccid보다 앞선다는 것을 먼저 확인해야 한다.!= HEAP_PAGE_VACUUM_NONE조건은 거친 사전 필터이며, 레코드 단위 검사가 실제 방어선이다. 이를 위반하면 활성 스냅샷이 해제된 slot을 가리키는 상황이 발생한다.
flowchart TD
A["heap_vacuum_all_objects\nnext_vpid = 헤더 페이지"] --> B{"VPID_ISNULL(next_vpid)?"}
B -- yes --> Z["unfix, free, return"]
B -- no --> C["pgbuf_ordered_fix(WRITE)"]
C --> D{"fix 성공?"}
D -- no --> Z
D -- yes --> E["heap_vpid_next -> next_vpid"]
E --> F{"n_objects>0 AND status != NONE?"}
F -- no --> G["watcher 교체, 루프"]
F -- yes --> H["OID[1..n] 구성, vacuum_heap_page"]
H --> I{"오류?"}
I -- yes --> Z
I -- no --> G
G --> B
Figure 8-1. heap_vacuum_all_objects 페이지 순회, 모든 분기.
8.2 spage_vacuum_slot: 살아있는 slot을 회수 가능한 공간으로 전환
섹션 제목: “8.2 spage_vacuum_slot: 살아있는 slot을 회수 가능한 공간으로 전환”vacuum_heap_record는 죽은 버전 하나당 spage_vacuum_slot을 한 번씩
호출한다. 해제된 바이트를 total_free에 반환하지만 compact는 수행하지
않는다.
// spage_vacuum_slot -- src/storage/slotted_page.c SPAGE_SLOT *slot_p = spage_find_slot (page_p, page_header_p, slotid, false); if (slot_p->record_type == REC_MARKDELETED || slot_p->record_type == REC_DELETED_WILL_REUSE) vacuum_er_log_error (..., "... was already vacuumed", ...); /* <- double-vacuum: log, continue */ page_header_p->num_records--; /* <- one fewer live record */ waste = DB_WASTED_ALIGN (slot_p->record_length, page_header_p->alignment); page_header_p->total_free += slot_p->record_length + waste; /* <- bytes returned to the page */ slot_p->offset_to_record = SPAGE_EMPTY_OFFSET; /* <- slot now points at nothing */ if (reusable) slot_p->record_type = REC_DELETED_WILL_REUSE;/* <- no refs: OID can be recycled */ else slot_p->record_type = REC_MARKDELETED; /* <- refs may exist: keep slot id reserved */reusable == false이면 slot은 REC_MARKDELETED가 되고 slot id는 예약된
채로 유지된다. 외부 참조(이전 버전의 prev_version_lsa 체인, 또는 relocation)가
재활용된 레코드가 아닌 tombstone으로 여전히 resolve되어야 하기 때문이다.
이미 vacuum된 분기는 중단-후-재실행 시나리오(8.4절)를 방어적으로 허용한다.
이 함수가 남기는 것: offset_to_record는 SPAGE_EMPTY_OFFSET이지만 slot 배열
항목은 제거되지 않고 레코드 바이트도 여전히 존재한다 — total_free만
증가시켰을 뿐이다. slot 배열을 줄이는 것은 spage_reclaim(8.5절)의 역할이며,
데이터 영역의 빈 공간을 회수하는 것은 spage_compact(9장)의 역할이다.
8.3 HEAP_PAGE_VACUUM_STATUS 상태 머신
섹션 제목: “8.3 HEAP_PAGE_VACUUM_STATUS 상태 머신”Vacuum 워커가 여전히 방문할 수 있는 힙 페이지는 해제하면 안 된다. CUBRID는
HEAP_CHAIN.flags의 상위 2비트에 세 가지 상태 플래그를 두어 이를 예측한다
(chain 구조체 전체는 1장에서 표로 정리했다).
// HEAP_PAGE_VACUUM_STATUS enum -- src/storage/heap_file.hHEAP_PAGE_VACUUM_NONE, /* Heap page is completely vacuumed. */HEAP_PAGE_VACUUM_ONCE, /* Heap page requires one vacuum action. */HEAP_PAGE_VACUUM_UNKNOWN /* Heap page requires an unknown number of vacuum actions. */
// flag bits -- src/storage/heap_file.c#define HEAP_PAGE_FLAG_VACUUM_STATUS_MASK 0xC0000000#define HEAP_PAGE_FLAG_VACUUM_ONCE 0x80000000#define HEAP_PAGE_FLAG_VACUUM_UNKNOWN 0x40000000HEAP_PAGE_GET_VACUUM_STATUS는 해당 비트들을 디코딩한다(둘 다 0이면 NONE);
HEAP_PAGE_SET_VACUUM_STATUS는 마스크를 지운 뒤 해당 비트를 OR한다.
flags의 하위 30비트는 다른 속성을 담는다. 상태 머신을 구동하는 HEAP_CHAIN
필드는 두 가지다:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
flags (상위 2비트) | 현재 vacuum 상태 (NONE/ONCE/UNKNOWN) | 레코드를 스캔하지 않고도 남은 vacuum 방문 횟수를 알림 |
max_mvccid | 페이지에 적용된 MVCC 연산의 최대 MVCCID | UNKNOWN에서 벗어나는 유일한 탈출구: vacuum 기준선보다 오래됐으면 ⇒ 모든 owed vacuum 완료 |
max_mvccid의 의미는 상태마다 다르다: NONE에서는 낡은 값(다음 연산이 새 id보다
앞선다고 assert한 뒤 갱신); ONCE에서는 owed vacuum이 <= max_mvccid 버전을
대상으로 함; UNKNOWN에서는 탈출 조건(vacuum_is_mvccid_vacuumed(max_mvccid)가
true이면 owed vacuum 전부 완료). MVCC 연산이 상태를 앞으로 밀고, vacuum이 뒤로
당긴다:
// heap_page_update_chain_after_mvcc_op -- src/storage/heap_file.cswitch (vacuum_status) {case HEAP_PAGE_VACUUM_NONE: assert (MVCC_ID_PRECEDES (chain->max_mvccid, mvccid)); HEAP_PAGE_SET_VACUUM_STATUS (chain, HEAP_PAGE_VACUUM_ONCE); break; /* <- first op since clean */case HEAP_PAGE_VACUUM_ONCE: HEAP_PAGE_SET_VACUUM_STATUS (chain, HEAP_PAGE_VACUUM_UNKNOWN); break; /* <- future unpredictable */case HEAP_PAGE_VACUUM_UNKNOWN: if (vacuum_is_mvccid_vacuumed (chain->max_mvccid)) /* <- all prior owed vacuum ran */ HEAP_PAGE_SET_VACUUM_STATUS (chain, HEAP_PAGE_VACUUM_ONCE); break; /* <- else: stays UNKNOWN */}if (MVCC_ID_PRECEDES (chain->max_mvccid, mvccid)) chain->max_mvccid = mvccid; /* <- track the max */
// heap_page_set_vacuum_status_none -- src/storage/heap_file.cassert (HEAP_PAGE_GET_VACUUM_STATUS (chain) == HEAP_PAGE_VACUUM_ONCE); /* <- only ONCE -> NONE */HEAP_PAGE_SET_VACUUM_STATUS (chain, HEAP_PAGE_VACUUM_NONE);stateDiagram-v2 [*] --> NONE NONE --> ONCE: mvcc 연산 \n max_mvccid가 새 id보다 앞선다고 assert ONCE --> UNKNOWN: 두 번째 mvcc 연산 \n 이후 예측 불가 ONCE --> NONE: vacuum 방문 \n set_vacuum_status_none UNKNOWN --> ONCE: mvcc 연산 AND max_mvccid 이미 vacuumed UNKNOWN --> UNKNOWN: mvcc 연산 AND max_mvccid 아직 미완료 NONE --> [*]: 페이지 해제 가능
Figure 8-2. HEAP_PAGE_VACUUM_STATUS 전이. 해제로 가는 유일한 탈출 엣지는 NONE에서만 나온다.
UNKNOWN은 NONE으로 직접 붕괴하지 않는다: 두 MVCC 연산이 vacuum 사이에
누적되면 CUBRID는 owed 방문 횟수 계산을 포기한다. 오직 새로운 연산이
max_mvccid가 vacuum 기준선을 넘겼다고 관측할 때만 ONCE로 내려오며,
ONCE에서 vacuum 방문 한 번으로 NONE으로 돌아온다.
불변 조건 — 페이지는
NONE상태에서만 해제할 수 있다. Vacuum은ONCE에서만 페이지를NONE으로 만들며,UNKNOWN에서 직접NONE으로 전환하지 않는다.heap_page_set_vacuum_status_none은 이전 상태가ONCE임을 assert한다. 따라서NONE은 남은 vacuum 방문이 없다는 의미 — 이것이heap_remove_page_on_vacuum이 의존하는 전제 조건이다. 다른 상태에서NONE에 도달하면 미래의 워커가 여전히 접근할 slot이 해제되어 use-after-free가 발생한다.
8.4 상태와 회수의 연결: vacuum_heap_page 결정 로직
섹션 제목: “8.4 상태와 회수의 연결: vacuum_heap_page 결정 로직”vacuum_heap_page는 레코드 단위 작업을 마친 후 상태를 읽어, NONE으로
전환할지 그리고 페이지 제거를 시도할지 결정한다:
// vacuum_heap_page (decision tail) -- src/query/vacuum.cpage_vacuum_status = heap_page_get_vacuum_status (thread_p, helper.home_page);assert (page_vacuum_status != HEAP_PAGE_VACUUM_NONE || (was_interrupted && helper.n_vacuumed == 0));if ((page_vacuum_status == HEAP_PAGE_VACUUM_ONCE && !was_interrupted) || (page_vacuum_status == HEAP_PAGE_VACUUM_NONE && was_interrupted)) { if (page_vacuum_status == HEAP_PAGE_VACUUM_ONCE) heap_page_set_vacuum_status_none (thread_p, helper.home_page); /* <- ONCE -> NONE, logged */ // ... pgbuf_set_dirty (home_page) ... if (spage_number_of_records (helper.home_page) <= 1 && helper.reusable) { /* <- only header slot, reusable heap */ if (pgbuf_has_prevent_dealloc (helper.home_page) == false && heap_remove_page_on_vacuum (thread_p, &helper.home_page, &helper.hfid)) { /* page gone */ goto end; } } }두 분기가 조건에 맞는다: ONCE && !was_interrupted(정상 경로 — NONE으로
전환 후 제거 시도)와 NONE && was_interrupted(크래시 이전 이미 vacuum이 완료된
재실행 태스크 — 이미 NONE이므로 재전환 없음). ONCE && was_interrupted는
의도적으로 제외된다: 재실행된 워커는 해당 ONCE가 자신의 태스크인지 크래시
이후에 기록된 새로운 삭제인지 판별할 수 없으므로, 전환하면 owed vacuum이
사라질 수 있다. UNKNOWN은 절대 매칭되지 않는다. 제거는 헤더 slot만 남은
페이지, reusable 힙, 스캐너가 pin하지 않은 경우에만 시도된다.
8.5 spage_reclaim: don’t-reuse 페이지의 slot 배열 압축
섹션 제목: “8.5 spage_reclaim: don’t-reuse 페이지의 slot 배열 압축”spage_vacuum_slot은 죽은 slot 배열 항목을 그대로 남긴다. spage_reclaim은
ANCHORED_DONT_REUSE_SLOTS 페이지의 slot 배열을 줄이는 역할로,
steady-state vacuum 루프가 아닌 reclaim-addresses 경로
(xheap_reclaim_addresses, compactdb가 진입)에서 호출된다.
// spage_reclaim -- src/storage/slotted_page.c if (page_header_p->num_slots > 0) { first_slot_p = spage_find_slot (page_p, page_header_p, 0, false); for (slot_id = page_header_p->num_slots - 1; slot_id >= 0; slot_id--) { /* <- backwards */ slot_p = first_slot_p - slot_id; if (slot_p->offset_to_record == SPAGE_EMPTY_OFFSET && (slot_p->record_type == REC_MARKDELETED || slot_p->record_type == REC_DELETED_WILL_REUSE)) { assert (page_header_p->anchor_type == ANCHORED_DONT_REUSE_SLOTS); if ((slot_id + 1) == page_header_p->num_slots) spage_reduce_a_slot (page_p); /* <- trailing dead slot: drop array entry */ else slot_p->record_type = REC_DELETED_WILL_REUSE; /* <- interior: mark recyclable */ is_reclaim = true; } } } if (is_reclaim == true) { if (page_header_p->num_slots == 0) spage_initialize (thread_p, page_p, ...); /* <- fully empty: reset page to pristine layout */ pgbuf_set_dirty (thread_p, page_p, DONT_FREE); } return is_reclaim;핵심 분기는 역방향 반복이다. 끝에 있는 죽은 slot만 spage_reduce_a_slot
(배열 길이를 줄임)으로 제거할 수 있으므로, 마지막 slot부터 0을 향해 거꾸로
순회하면 제거할 때마다 다음 끝 죽은 slot이 드러난다. 내부의 죽은 slot은 뒤에
있는 살아있는 slot을 재번호매기지 않고서는 제거할 수 없기 때문에(OID가 무효화됨)
REC_DELETED_WILL_REUSE로 재표시하여 미래의 insert가 재사용하게 한다.
종료 분기: 진입 시 num_slots == 0이면 false 반환; 무언가 회수되어 페이지가
완전히 비었으면 spage_initialize가 초기화; 아무것도 회수되지 않았으면
페이지는 그대로 유지된다.
spage_vacuum_slot과의 비대칭이 핵심 포인트다: vacuum은 spage_vacuum_slot으로
공간을 지속적으로 해제하고 vacuum_log_vacuum_heap_page로 기록한다.
반면 spage_reclaim은 더 무거운 reclaim-addresses 경로에서, don’t-reuse anchor에
대해서만 slot 배열 항목을 회수한다. 이 경로에서는 xheap_reclaim_addresses가
reclaim 로깅을 의도적으로 건너뛴다(log_skip_logging, if (spage_reclaim (...) == true) 블록 내)
— 참조되지 않는 죽은 OID를 재사용해도 데이터베이스를 논리적으로 변경하지 않으므로
REDO도 UNDO도 불필요하기 때문이다.
8.6 heap_remove_page_on_vacuum: 빈 페이지 연결 해제
섹션 제목: “8.6 heap_remove_page_on_vacuum: 빈 페이지 연결 해제”페이지가 NONE 상태에 도달하고 헤더 slot만 남으면, vacuum은 해당 페이지를
파일 관리자에게 반환하려 시도한다. 이 함수는 최소 침습적으로 설계되어 있다:
모든 이웃 페이지는 ordered watcher로 조건부로 fix되며, 실패하면
시도를 포기한다(return false).
// heap_remove_page_on_vacuum -- src/storage/heap_file.c assert (spage_number_of_records (*page_ptr) <= 1); /* <- precondition: page is empty */ pgbuf_get_vpid (*page_ptr, &page_vpid); if (page_vpid.pageid == hfid->hpgid && page_vpid.volid == hfid->vfid.volid) return false; /* <- never remove the header page */ if (pgbuf_ordered_fix (thread_p, &header_vpid, OLD_PAGE, PGBUF_LATCH_WRITE, &header_watcher) != NO_ERROR) goto error; /* <- give up if header busy */ // ... heap_vpid_prev/next, conditional fix of prev (unless == header) and next ... if (crt_watcher.page_was_unfixed) { *page_ptr = crt_watcher.pgptr; /* <- ordered fix may have refixed home */ if (spage_number_of_records (crt_watcher.pgptr) > 1) goto error; /* <- re-filled while we waited */ } if (pgbuf_has_prevent_dealloc (crt_watcher.pgptr)) goto error; /* <- a scanner reached us */ if (pgbuf_has_any_waiters (crt_watcher.pgptr)) { assert (false); goto error; } log_sysop_start (thread_p); is_system_op_started = true; /* <- atomic unlink begins */ // ... scrub vpid from estimates.best[]/second_best[]/last_vpid/full_search_vpid, // splice prev.next_vpid=next and next.prev_vpid=prev (RVHF_STATS / RVHF_CHAIN logs) ... pgbuf_ordered_unfix_and_init (thread_p, *page_ptr, &crt_watcher); if (file_dealloc (thread_p, &hfid->vfid, &page_vpid, FILE_HEAP) != NO_ERROR) goto error; (void) heap_stats_del_bestspace_by_vpid (thread_p, &page_vpid); /* <- evict from cache */ log_sysop_commit (thread_p); is_system_op_started = false; return true;error: if (is_system_op_started) log_sysop_abort (thread_p); /* <- roll back half-done unlink */ // ... unfix any fixed watchers; return false ...모든 분기는 “안전하게 포기” 경로다(Figure 8-3). 이 함수는 페이지를 절반만
연결 해제하는 일이 없다. 이웃 페이지가 바쁘면 제거를 지연시킬 뿐이고,
page_was_unfixed에서 레코드가 1개 초과이면 watcher를 VPID 순서로 획득하는
사이 동시 insert가 페이지를 다시 채운 것이며, has_any_waiters는 불가능한
상황이다(assert(false)). 정상 경로는 헤더의 best-space 추정치에서 페이지를
제거하고, 체인을 이어붙이며(prev.next_vpid = next, next.prev_vpid = prev,
이 페이지가 파일의 첫 번째 데이터 페이지였다면 heap_hdr.next_vpid = next),
하나의 system op 안에서 file_dealloc을 호출한다.
불변 조건 — 체인 splice와 dealloc은 원자적이다. 헤더 stats, prev 체인, next 체인의
spage_update와file_dealloc은 하나의log_sysop_start/log_sysop_commitop 안에서 실행된다(RVHF_STATS/RVHF_CHAIN). 크래시가 splice 도중 발생하면 전부 아니면 전무로 복구된다; 절반만 연결된 체인은 순회를 망가뜨리는데, 이를 sysop가 막는다.
flowchart TD
A["remove_page_on_vacuum\nassert 레코드 <=1"] --> B{"헤더 페이지?"}
B -- yes --> R0["return false"]
B -- no --> C["ordered_fix 헤더"]
C --> D{"fix 성공?"}
D -- no --> E["goto error -> false"]
D -- yes --> F["vpid_prev / vpid_next"]
F --> G["prev, next 조건부 fix"]
G --> H{"home 재충전, >1 레코드?"}
H -- yes --> E
H -- no --> I{"prevent_dealloc 또는 waiters?"}
I -- yes --> E
I -- no --> J["sysop_start"]
J --> K["bestspace 제거, prev/next splice, file_dealloc"]
K --> L{"모두 성공?"}
L -- no --> M["sysop_abort -> false"]
L -- yes --> N["sysop_commit, bestspace 캐시 삭제, return true"]
Figure 8-3. heap_remove_page_on_vacuum — 모든 경로는 실패 안전(fail-safe)이다.
8.7 Best-space 캐시와 HEAP_DROP_FREE_SPACE 임계값
섹션 제목: “8.7 Best-space 캐시와 HEAP_DROP_FREE_SPACE 임계값”Best-space 메커니즘의 상세는 9장에서 다룬다. Vacuum은 두 접점에서 이를
건드린다. HEAP_DROP_FREE_SPACE(heap_file.h, (int)(DB_PAGESIZE * 0.3))는
페이지를 insert 대상으로 광고할 만큼 충분한 여유 공간 수준이다;
spage_vacuum_slot이 이 임계값을 넘을 만큼 바이트를 반환하면 페이지가
best-space 힌트에 다시 올라올 수 있다. 반대로 heap_remove_page_on_vacuum이
페이지를 해제하면 캐시에서 제거해야 한다(estimates.best[]/second_best[]의
해당 항목을 null로 만들고 heap_stats_del_bestspace_by_vpid를 호출), 미래의
insert가 해제된 VPID를 받지 않도록 한다.
8.8 REC_MVCC_NEXT_VERSION(레거시) 대 prev_version_lsa
섹션 제목: “8.8 REC_MVCC_NEXT_VERSION(레거시) 대 prev_version_lsa”과거 CUBRID는 레코드의 후속 버전을 가리키는 forward pointer를 힙 내부에
REC_MVCC_NEXT_VERSION slot으로 저장했고, vacuum이 이를 따라가야 했다. 현재
코드는 이를 prev_version_lsa로 대체했다: update가 새 레코드의 MVCC 헤더에
이전 버전의 로그 LSA를 기록한다(heap_update_set_prev_version 및 5-6장 참고).
덕분에 버전 체인이 힙이 아닌 로그에 존재한다. 따라서 vacuum은 이제 후속 버전을
회수하기 위해 forwarding link를 따라갈 필요가 없다 — 죽은 버전의 slot을
spage_vacuum_slot으로 제자리에서 tombstone 처리하여 바이트를 회수하고,
이전 이미지가 필요한 reader는 prev_version_lsa를 따라 로그로 이동한다.
이것이 reusable 힙에서 spage_vacuum_slot이 즉시 REC_DELETED_WILL_REUSE를
선택할 수 있는 이유이기도 하다: 힙 내부에 next-version 링크가 없으므로
해당 slot을 참조하는 것이 없다.
8.9 챕터 요약 — 핵심 정리
섹션 제목: “8.9 챕터 요약 — 핵심 정리”heap_vacuum_all_objects는 페이지를,vacuum_heap_page는 slot을 선택한다 — 드라이버는next_vpid체인을 순회하며HEAP_PAGE_VACUUM_NONE페이지를 건너뛰고, 레코드 단위threshold_mvccid사망 테스트를 아래로 위임한다.spage_vacuum_slot은 바이트를 해제하지 slot 항목을 해제하지 않는다 —total_free를 증가시키고,offset_to_record = SPAGE_EMPTY_OFFSET으로 설정하며,REC_DELETED_WILL_REUSE(reusable) 또는REC_MARKDELETED(참조 가능) 도장을 찍는다. compact는 수행하지 않는다.NONE → ONCE → UNKNOWN머신은 owed vacuum 방문을 예측한다 — MVCC 연산이 앞으로 밀고, vacuum 방문만이 뒤로 당긴다(ONCE → NONE만),UNKNOWN은max_mvccid가 vacuum 기준선 아래로 떨어질 때만 탈출한다.- 페이지는
NONE상태에서만 해제할 수 있다 —heap_remove_page_on_vacuum이 미래 워커가 해제된 slot을 건드리는 상황을 피하기 위해 의존하는 전제 조건이다. spage_reclaim은ANCHORED_DONT_REUSE_SLOTS전용으로 slot 배열을 역방향으로 압축한다, reclaim 경로에서xheap_reclaim_addresses가 로깅을 건너뛴다; 끝에 있는 tombstone은spage_reduce_a_slot으로 제거하고, 내부 것은 재사용 가능으로 표시한다.heap_remove_page_on_vacuum은 실패 안전하고 원자적이다 — 모든 경합 케이스는 중단(return false)하며, 체인 splice와file_dealloc은 하나의 로깅된 system op (all-or-nothing 복구)으로 실행된다.prev_version_lsa가 힙 내부의REC_MVCC_NEXT_VERSION링크를 대체했다, vacuum은 죽은 버전의 slot을 제자리에서 회수한다; 해제된 공간이HEAP_DROP_FREE_SPACE(30%)를 넘으면 best-space 캐시에 다시 공급된다.
Chapter 9: Best Space와 빈 공간 관리
섹션 제목: “Chapter 9: Best Space와 빈 공간 관리”4장(삽입)과 6장(갱신)에서 미뤄 둔 함수가 하나 있다. “여유 공간이 충분한 페이지를 골라라” — heap_stats_find_best_page가 그것이다. 이 장에서는 그 기저 구조를 해부한다. 온디스크 힌트(HEAP_HDR_STATS.estimates), 인메모리 캐시(heap_Bestspace), 그리고 전체 힙을 스캔하지 않고도 그림을 최신 상태로 유지하는 지연 조정(heap_stats_sync_bestspace)을 차례로 다룬다.
핵심 아이디어: 빈 공간 회계는 의도적으로 근사치다. 여기서 다루는 내용 중 정확성을 위해 로깅되는 것은 없다(log_skip_logging 호출이 그 증거다). 항목은 낡을 수 있고, 힌트가 틀려도 기껏해야 페이지 fix를 한 번 헛되이 할 뿐 데이터 오류는 일어나지 않는다. 힙이 빈 공간 힌트를 유지하는 이유는 cubrid-heap-manager.md의 “Free-space management” 절을 보라. 이 장은 그 구현이다.
9.1 두 계층 힌트 모델
섹션 제목: “9.1 두 계층 힌트 모델”두 저장소를 순서대로 조회한다. (1) 인메모리 캐시 heap_Bestspace(HEAP_STATS_BESTSPACE_CACHE) — HFID와 VPID를 키로 삼는 프로세스 전역 해시, PRM_ID_HF_MAX_BESTSPACE_ENTRIES로 상한을 지정한다(<= 0이면 비활성화). (2) 힙 헤더 HEAP_HDR_STATS.estimates의 온디스크 힌트 — best[10] 링, second_best[10] 링, 그리고 재시작 이후에도 유지되는 집계 카운터. heap_stats_find_best_page는 두 계층을 모두 읽고, heap_stats_sync_bestspace는 두 계층을 모두 재구성한다.
9.2 heap_bestspace와 estimates 블록 — 모든 필드
섹션 제목: “9.2 heap_bestspace와 estimates 블록 — 모든 필드”heap_bestspace(heap_file.h)는 힌팅의 원자 단위로, 후보 페이지 하나와 그 페이지의 예상 여유 공간을 담는다.
// heap_bestspace -- src/storage/heap_file.hstruct heap_bestspace{ VPID vpid; /* Vpid of one of the best pages */ int freespace; /* Estimated free space in this page */};| 필드 | 역할 | 존재 이유 |
|---|---|---|
vpid | 후보 페이지 ID | 디렉터리 탐색 없이 페이지를 직접 fix |
freespace | 예상 여유 바이트 | 사전 필터: < needed이면 건너뜀; 지연 평가이므로 부정확할 수 있음 |
이 구조체는 세 가지 형태로 나타난다. 온디스크 best[] 링의 원소, 인메모리 HEAP_STATS_ENTRY.best, 그리고 선택 함수 내 지역 변수. 온디스크 링은 heap_hdr_stats(heap_file.c)의 익명 estimates 부분 구조체 안에 있으며, 그 모든 필드를 정리하면 다음과 같다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
num_pages | 대략적인 페이지 수 | 유계 스캔 min(20%, 100) 크기를 결정 |
num_recs | 대략적인 레코드 수 | heap_stats_get_min_freespace에서 평균 레코드 크기 계산에 사용 |
recs_sumlen | 레코드 길이 합 근사값 | 평균 레코드 크기 추정의 분자 |
num_other_high_best | best[]에 없는 양호 페이지 수 | 재동기화 게이트: /num_pages >= 0.1일 때만 스캔 |
num_high_best | 아직 유효한 best[] 슬롯 수 | 0이면 소진; 동기화가 필요함을 의미 |
num_substitutions | best 슬롯 퇴거 횟수 | % 1000 == 0일 때 second_best[]에 하나 추가 |
num_second_best | 유효한 second_best[] 항목 수 | 링이 비었는지 가득 찼는지 구분 (head==tail이면 두 상태 모두 해당) |
head_second_best | second_best[] 소비 인덱스 | heap_stats_get_second_best가 여기서 팝 |
tail_second_best | second_best[] 생산 인덱스 | heap_stats_put_second_best가 여기에 푸시 |
head | best[] 소비/삽입 인덱스 | 다음 치환이 이 위치에 기록됨 |
last_vpid | 힙의 마지막 페이지 | heap_vpid_alloc이 새 페이지를 이 뒤에 연결 |
full_search_vpid | 유계 스캔 재개 커서 | 호출 간 스캔을 분산시킴 |
second_best[10] | 퇴거된 양호 페이지 저수지 링 | 예비 풀; 한 번에 전부 재사용하지 않음 |
best[10] | 1차 후보 링 | 핫 패스에서 가장 먼저 읽힘 |
불변 조건 — 링 일관성. head는 [0, 10) 범위를 유지하며 HEAP_STATS_NEXT_BEST_INDEX(i)=(i+1)%10으로만 전진한다. head가 범위를 벗어나면 배열 밖을 읽게 된다. second_best는 (tail - head + 10) % 10 == num_second_best가 성립해야 한다(put/get 헬퍼에서 단언). num_second_best는 링이 가득 찼는지 비었는지 구분하는 유일한 수단이다(두 상태 모두 head==tail이 될 수 있기 때문). 이 값들은 로깅되지 않으므로 순수하게 실행 중 상태다.
9.3 임계값: HEAP_DROP_FREE_SPACE, unfill_space, 최소 여유 공간
섹션 제목: “9.3 임계값: HEAP_DROP_FREE_SPACE, unfill_space, 최소 여유 공간”HEAP_DROP_FREE_SPACE(heap_file.h)는 (int)(DB_PAGESIZE * 0.3) — 30% 허용 하한선이다. 여유 공간이 페이지의 ~30% 이상인 경우에만 캐시에 등록되고, 그 이하면 버려진다(hence “drop free space”). unfill_space는 페이지당 예비 여유 공간(DB_PAGESIZE * PRM_ID_HF_UNFILL_FACTOR, 힙 생성 시 결정)이다. 선택자는 이 값을 요청 크기에 더해서, 현재 레코드뿐 아니라 미래의 제자리(in-place) 갱신 성장분까지 페이지가 수용할 수 있도록 한다(6장: 커지는 갱신은 같은 페이지에 머물기를 선호). heap_stats_get_min_freespace는 두 값을 결합한다. 평균 레코드 크기(recs_sumlen/num_recs, num_recs == 0이면 header_size + 20)에 unfill_space를 더한 뒤, MIN(..., HEAP_DROP_FREE_SPACE)로 상한을 제한한다.
불변 조건 — 최소 여유 공간은 drop 하한을 초과하지 않는다. 마지막의 MIN(..., HEAP_DROP_FREE_SPACE) 처리가 30%를 넘기는 모든 페이지를 후보로 유지시킨다. 이 처리가 없으면 대형 레코드가 min-freespace를 30%보다 높게 밀어올려, 동기화(30%만 확인)가 기록한 페이지를 updater가 거부하는 영구적인 불일치가 생긴다.
9.4 heap_stats_find_page_in_bestspace — 힌트 스캔
섹션 제목: “9.4 heap_stats_find_page_in_bestspace — 힌트 스캔”내부 루프: best[](호출자 제공)와 캐시를 주어진 상태에서, LK_FORCE_ZERO_WAIT를 사용해 >= needed_space를 만족하는 X-latch 고정 페이지를 반환하거나 HEAP_FINDSPACE_NOTFOUND/_ERROR를 반환한다. 바쁜 페이지는 대기하지 않고 건너뛴다.
// heap_stats_find_page_in_bestspace -- src/storage/heap_file.cwhile (notfound_cnt < BEST_PAGE_SEARCH_MAX_COUNT /* cap self-heal at 100 misses */ && (ent = mht_get2 (heap_Bestspace->hfid_ht, hfid, NULL)) != NULL) { if (ent->best.freespace >= needed_space) { best = ent->best; break; } /* hit */ mht_rem2 (...); mht_rem (...); /* stale: evict from both ht */ heap_stats_entry_free (thread_p, ent, NULL); heap_Bestspace->num_stats_entries--; notfound_cnt++; }캐시 조회는 자기 치유(self-heal)한다 — 공간이 부족한 힌트는 두 해시 테이블에서 모두 퇴거된다. 캐시 미스(또는 캐시 비활성화) 시에는 best.freespace가 -1로 남고, 코드는 best_array_index부터 온디스크 best[]를 스캔한다. 이때 best_hint_is_used를 설정해, 수정된 freespace를 나중에 다시 기록한다. 각 후보는 한 번의 fix-and-recheck 사이클을 거친다. 수정자가 반드시 지켜야 할 분기 규칙:
- 미스 시에도 재확인과 힌트 갱신을 수행한다.
spage_max_space_for_new_record가 진실이다. 공간이 부족하면 unfix하고 계속 진행하되, 힌트/캐시는 갱신한다. - 반환되는 freespace에는 unfill이 포함되지 않는다 —
record_length + heap_Slotted_overhead만 차감한다. 예비 공간은 이미needed_space에 포함되어 있다. - 오류 vs. 타임아웃.
er_errid()==NO_ERROR인NULLfix는 제로 대기 타임아웃이다(다음으로 넘어감).ER_INTERRUPTED는 중단을 의미한다. 그 밖의 오류는 힌트를 버리고assert(false)와 함께_ERROR를 반환한다. idx_badspace출력 파라미터는 여유 공간이 가장 적은 슬롯을 돌려준다. 호출자는 이를 새head로 설정해 다음 치환이 최악의 슬롯을 덮어쓰도록 한다.
9.5 heap_stats_find_best_page — 오케스트레이터
섹션 제목: “9.5 heap_stats_find_best_page — 오케스트레이터”4장과 6장에서 호출한 함수. 레코드 예상치를 먼저 갱신한다(삽입 시 num_recs += 1; 항상 recs_sumlen += needed_space). 그런 다음 힌트 스캔, 동기화, 할당을 루프한다. 코드 안에 미묘한 점 두 가지가 있다.
// heap_stats_find_best_page -- src/storage/heap_file.ctotal_space = needed_space + heap_Slotted_overhead + heap_hdr->unfill_space;if (heap_is_big_length (total_space)) /* unfill would overflow page */ total_space = needed_space + heap_Slotted_overhead; /* -> drop the reserve */if (try_find >= 2 || other_high_best_ratio < HEAP_BESTSPACE_SYNC_THRESHOLD) /*0.1f*/ break; /* sync-admission gate */모든 분기: (1) 헤더 fix 실패 → goto error, NULL 반환. (2) 그 외 루프(try_find++). (3) heap_stats_find_page_in_bestspace → _ERROR이면 unfix 후 goto error; 페이지를 찾으면 완료. (4) 페이지 없음 → 게이트 평가(num_pages<=0 또는 num_other_high_best<=0이면 ratio 0으로 강제, assert(num_pages>0); try_find>=2; ratio<0.1) 통과 시 할당으로 이동. (5) 게이트 통과 → syncloop에서 heap_stats_sync_bestspace(false,true)를 found가 0인 동안 최대 3회 수행 후 재스캔; num_pages_found<0은 하드 오류, 재시도 후에도 <=0이면 alloc. (6) 여전히 NULL → heap_vpid_alloc(실패 시 goto error); 그 외 종료: log_skip_logging 헤더, dirty + free 설정 후 pgptr 반환.
불변 조건 — 헤더는 선택 전 과정에서 WRITE로 유지된다. 예상치를 처음 건드리기 전에 PGBUF_LATCH_WRITE로 fix되며, 단 하나의 dirty-and-free 종료 지점(또는 각 goto error)에서만 해제된다. 예상치 변경, 동기화, 할당이 모두 이 단일 latch 아래서 이루어지므로, 두 삽입자가 head/num_high_best를 동시에 오염시킬 수 없다. 종료 시점의 log_skip_logging은 변경 사항을 로깅하지 않으므로 자기 치유 방식으로 복구된다.
9.6 heap_stats_update — 삭제/갱신 이후 페이지를 양호 상태로 표시
섹션 제목: “9.6 heap_stats_update — 삭제/갱신 이후 페이지를 양호 상태로 표시”삭제 또는 제자리 축소로 공간이 확보되면 해당 페이지는 재사용 후보가 될 수 있다. heap_stats_update는 이를 알리는 경량 알리미다(삭제/갱신 경로와 복구 redo에서 호출, 10장).
// heap_stats_update -- src/storage/heap_file.cfreespace = spage_get_free_space_without_saving (thread_p, pgptr, &need_update);if (PRM_ID_HF_MAX_BESTSPACE_ENTRIES > 0 && prev_freespace < freespace) heap_stats_add_bestspace (thread_p, hfid, vpid, freespace); /* room grew: cache */if (need_update || prev_freespace <= HEAP_DROP_FREE_SPACE) if (freespace > HEAP_DROP_FREE_SPACE) { /* now genuinely good */ error = heap_stats_update_internal (...); /* try to write best[] */ if (error != NO_ERROR) spage_set_need_update_best_hint (..., true); /* defer */ else if (need_update) spage_set_need_update_best_hint (..., false); /* clear */ } else if (need_update) spage_set_need_update_best_hint (..., false); /* obsolete */분기: 캐시를 먼저 시도한다(활성화 상태이고 공간이 증가한 경우 — latch 불필요). 온디스크 갱신은 need_update이거나 prev_freespace <= 30%인 경우에만 수행한다. heap_stats_update_internal은 CONDITIONAL 헤더 latch를 시도한다 — 실패하면 need_update_best_hint = true로 표시(“양호한 페이지이지만 헤더에 알릴 수 없었음”). 이 페이지는 미래의 삭제나 전체 동기화가 재발견할 때까지 보이지 않는다(근사 비용). 성공하고 need_update이면 플래그를 지운다. 30% 이하로 내려간 플래그 페이지도 플래그를 지운다(더 이상 유효하지 않기 때문).
heap_stats_update_internal 내부에서, 퇴거된 30% 초과 슬롯은 heap_stats_put_second_best로 저수지에 추가될 수 있다. 여기에 주기가 있다: if (heap_hdr->estimates.num_substitutions++ % 1000 == 0) 가드가 1000번의 퇴거 중 한 번만 허용한다 — tail_second_best에 *vpid를 기록하고 tail(가득 찼으면 head도 함께)을 전진시키며 num_second_best를 증가시키고 카운터를 1로 초기화한다.
1-in-1000인 이유 (쓰기 분산). 대량 비우기 직후 모든 페이지를 캐시에 넣으면 다음 삽입들이 그 페이지들에 다시 몰려들어, 방금 비운 페이지를 다시 더럽히게 된다. 1000번에 하나씩 샘플링하면 재사용이 파일 전체에 분산되어, 비워진 익스텐트가 빈 상태로 남아 OS에 반환될 수 있다.
9.7 heap_stats_sync_bestspace — 유계 재구성
섹션 제목: “9.7 heap_stats_sync_bestspace — 유계 재구성”힌트가 고갈되면 이 함수가 체인을 따라 걷되 일부만 스캔함으로써 best[]를 재구성하고 카운터를 갱신한다 — “전체 힙을 스캔하지 않고도”라는 요건에 대한 답이다. 상한은 max_iterations = MAX(MIN((int)(num_pages*0.2), heap_Find_best_page_limit /*100*/), HEAP_NUM_BEST_SPACESTATS /*10*/): 최대 min(페이지의 20%, 100), 최소 10. 시드(seed) 캐스케이드: (1) 캐시 활성화 → search_all=true이고 full_search_vpid(재개 커서, 각 단계마다 전진하여 동기화가 파일을 20%-per-call로 순회)부터 시작; (2) 아니면 num_high_best > 0 → head 바로 앞; (3) 아니면 num_second_best > 0 → 저수지에서 팝(heap_stats_get_second_best); (4) 아니면 search_all=true이고 full_search_vpid부터, NULL이면 첫 페이지(hfid->hpgid)이고 can_cycle=false.
stateDiagram-v2 [*] --> Seed Seed --> Walk: 캐스케이드로 start_vpid 결정 Walk --> Inspect: 페이지 READ fix, 해제 방지 Inspect --> Walk: 여유 공간 30% 이하, 건너뜀 Inspect --> Record: 여유 공간 30% 초과 Record --> Walk: best[] 미충족, 저장 후 num_high_best++ Record --> Walk: best[] 충족, num_other_best++ Walk --> Commit: 반복 횟수 초과 OR num_high_best == 10 OR stopat_vpid 도달 Commit --> [*]: head, num_high_best, 카운터 재구성
Figure 9-3: heap_stats_sync_bestspace, 유계(scan_all=false) 동기화. 상한 도달 시 iterate_all을 설정하고 break. 시드 슬롯에서 아무것도 찾지 못한 경우(start_pos != -1 && num_high_best == 0)에는 NULL로 초기화해 다음 호출이 죽은 지점에서 다시 시드하지 않도록 한다. 페이지마다 spage_collect_statistics로 카운터를 누적하고, search_all이면 full_search_vpid를 갱신하며, can_cycle이 활성이면 힙 시작으로 wrap-around한다.
커밋 단계: 유계 스캔에서 아무것도 찾지 못하고 second_best도 비어 있으면 goto end로 조기 종료. 그 외에는 사용하지 않은 best[] 후미를 NULL로 초기화하고 head/num_high_best를 설정한다. scan_all이거나 num_pages >= stored이면 모든 카운터를 전면 덮어쓴다. 그 외의 보수적 병합(num_other_high_best -= num_high_best, num_other_best보다 크면 그 값으로 대체, 부분 스캔이 더 많이 보았을 경우에만 레코드 카운트 덮어쓰기).
불변 조건 — 동기화는 절대 로깅하지 않는다. 헤더 주석에 명시되어 있으며 호출자가 log_skip_logging으로 래핑한다. 동기화 도중 크래시가 나면 오래된 예상치가 남지만, 다음 삽입이 재동기화한다 — 따라서 undo 데이터가 없고, 체인 탐색에서는 reader가 레코드 잠금 없이 OLD_PAGE_PREVENT_DEALLOC, PGBUF_LATCH_READ로 접근한다.
9.8 할당 폴백: heap_vpid_alloc vs heap_alloc_new_page
섹션 제목: “9.8 할당 폴백: heap_vpid_alloc vs heap_alloc_new_page”재사용이 실패하면 오케스트레이터는 heap_vpid_alloc — 통계 인식 할당자 — 를 호출한다. file_alloc으로 페이지를 확보하고 last_vpid 뒤에 연결(RVHF_CHAIN 로깅)한 뒤 last_vpid/num_pages를 증가시키고, head 위치에 설치하며 head를 전진시킨다. 설치 분기 세 가지:
// heap_vpid_alloc -- src/storage/heap_file.cbest = heap_hdr->estimates.head;heap_hdr->estimates.head = HEAP_STATS_NEXT_BEST_INDEX (best);if (VPID_ISNULL (&heap_hdr->estimates.best[best].vpid)) heap_hdr->estimates.num_high_best++; /* slot was empty */else if (heap_hdr->estimates.best[best].freespace > HEAP_DROP_FREE_SPACE) { heap_hdr->estimates.num_other_high_best++; /* evict good page to reservoir */ heap_stats_put_second_best (heap_hdr, &heap_hdr->estimates.best[best].vpid); }heap_hdr->estimates.best[best].vpid = vpid;heap_hdr->estimates.best[best].freespace = DB_PAGESIZE; /* fresh page is all free */예상치 변경과 달리 체인 연결과 RVHF_STATS는 log_sysop_start/commit 안에서 로깅된다 — 체인에서 새 페이지를 잃으면 스토리지가 누수된다(10장). heap_alloc_new_page는 베어(bare) 할당자다(7번 핵심 정리): prev/next 링크 없이 file_alloc하고 watcher를 붙이되 estimates는 전혀 건드리지 않는다.
9.9 챕터 요약 — 핵심 정리
섹션 제목: “9.9 챕터 요약 — 핵심 정리”- 두 계층, 순서대로:
heap_stats_find_page_in_bestspace는 인메모리heap_Bestspace캐시를 먼저, 그 다음 온디스크best[10]링을 탐색한다. 두 계층 모두 접촉 시 낡은 항목을 자기 치유한다. heap_stats_find_best_page는 헤더 WRITE latch를 유지한 채로 오케스트레이션한다: 예상치 증가, 힌트 스캔, 조건부 동기화, 그래도 없으면heap_vpid_alloc. 게이트(try_find >= 2또는ratio < 0.1)는 스토리지 대비 CPU 비용의 조정 손잡이다.- 여기서 정확성을 위해 로깅되는 것은 없다 — 예상치 변경은
log_skip_logging하에, 동기화도 로깅하지 않는다.heap_vpid_alloc만이 체인 연결과RVHF_STATS를 로깅한다. - **
HEAP_DROP_FREE_SPACE(30%)는 허용 하한선,unfill_space**는 갱신 여유 공간으로needed_space에 접혀 들어가고 반환되는 freespace에서는 제외된다. - 유계 재개 가능 스캔: 동기화는
min(페이지의 20%, 100)만 검사하고,full_search_vpid를 시드로 삼아 그 커서를 전진시키므로, 삽입을 막지 않고 파일을 순회할 수 있다. - second-best 저수지는 쓰기를 분산시킨다: 퇴거된 양호 페이지는 1000번에 하나(
num_substitutions % 1000)만 허용되므로, 대량 비우기 직후에도 해당 영역이 드물게 재사용된다. heap_vpid_alloc(통계 인식) vsheap_alloc_new_page(베어): 전자는 페이지를best[head]로 설치하고last_vpid/num_pages를 유지한다. 후자는 예상치를 건드리지 않고, 페이지 체인을 직접 관리하는 호출자를 위해 존재한다.
Chapter 10: 크래시 복구와 Redo/Undo 로그 경로
섹션 제목: “Chapter 10: 크래시 복구와 Redo/Undo 로그 경로”이 챕터는 앞선 모든 흐름이 미뤄 두었던 질문에 답한다: 크래시 이후에는 무슨 일이 벌어지는가? 각 연산이 고정하는 redo/undo 핸들러를 추적하고, UPDATE의 undo 이미지가 prev_version_lsa가 가리키는 정확한 바이트임을 보여(챕터 6과 복구를 연결), 복구 전용 slotted-page 프리미티브를 해부한다. 이 경로는 **엣지 경로(edge path)**다: 새로운 레코드 상태를 만들지 않고, 순방향 경로가 생산한 상태를 재구성할 뿐이다. WAL / ARIES 이론은 cubrid-heap-manager.md의 §“Durability and the Write-Ahead Log”를 참고한다.
10.1 복구 디스패치 테이블
섹션 제목: “10.1 복구 디스패치 테이블”recovery.c의 RV_fun[]은 각 LOG_RCVINDEX를 {undofun, redofun, ...} 튜플에 매핑한다. 드라이버는 rcv->pgptr(고정된 페이지), rcv->offset(slotid, vacuum 플래그와 OR된 경우 있음), rcv->length / rcv->data(페이로드), rcv->mvcc_id를 담은 LOG_RCV와 함께 매핑된 함수를 호출한다. 모든 핸들러를 규정하는 두 가지 사실이 있다: 물리적으로 동일한 연산에는 하나의 redo 핸들러가 대응한다(heap_rv_redo_insert는 RVHF_INSERT와 RVHF_INSERT_NEWHOME 모두를 redo), undo는 역 연산의 redo다 — heap은 insert/delete에 대한 별도 undo 이미지를 로그에 남기지 않으므로, slotid와 redo 페이로드만으로 되돌린다.
flowchart TB
LOG["로그 레코드\nrcvindex + LOG_RCV{pgptr, offset, data, mvcc_id}"]
DISP["복구 드라이버\nRV_fun[rcvindex] 조회"]
LOG --> DISP
DISP -->|redo 패스| REDO["redofun\nheap_rv_redo_* / heap_rv_mvcc_redo_*"]
DISP -->|undo 패스| UNDO["undofun\nheap_rv_undo_* / heap_rv_mvcc_undo_*"]
REDO --> SPI["spage_insert_for_recovery /\nspage_update / spage_delete"]
UNDO --> SPD["spage_delete_for_recovery /\nspage_update"]
SPI --> DIRTY["pgbuf_set_dirty"]
SPD --> DIRTY
Figure 10-1 — 로그 레코드가 slotted-page 프리미티브에 도달하는 경로.
10.2 공유 vacuum 플래그 전처리
섹션 제목: “10.2 공유 vacuum 플래그 전처리”순방향 경로(heap_mvcc_log_insert, heap_mvcc_log_delete, heap_mvcc_log_home_change_on_delete)는 연산이 페이지의 vacuum 상태를 함께 변경했을 때 p_addr->offset에 HEAP_RV_FLAG_VACUUM_STATUS_CHANGE(0x8000)를 OR한다. 따라서 모든 MVCC 핸들러는 동일한 마스킹 구문으로 시작하며, 이후에는 // ... mask vacuum flag, see 10.2 ...로 생략한다:
// shared preamble in every MVCC handler -- src/storage/heap_file.cif (slotid & HEAP_RV_FLAG_VACUUM_STATUS_CHANGE) { vacuum_status_change = true; }slotid = slotid & (~HEAP_RV_FLAG_VACUUM_STATUS_CHANGE); /* recover real slotid */이 비트는 페이지 체인에 상태 변경을 전파할지 여부를 결정하고(챕터 8), 마스크를 적용해 실제 slotid를 복원한다.
불변 조건 — 복구된 slotid는 vacuum 플래그를 slotted-page 호출로 전달하지 않는다. 마스킹을 빠뜨리면 slotid가 32768 이상이 되어
spage_find_slot이 실패한다. 레코드 재구성 핸들러(heap_rv_mvcc_redo_insert,heap_rv_undoredo_update,heap_rv_redo_update_and_update_chain,heap_rv_mvcc_undo_delete)는assert (slotid > 0)을 경계 조건으로 사용하며,heap_rv_undo_insert는 삭제만 수행하므로 assertion 없이 마스킹만 하고spage_delete_for_recovery가 잘못된 slotid를 자체적으로 거부한다.
10.3 INSERT 복구 — heap_rv_redo_insert와 heap_rv_mvcc_redo_insert
섹션 제목: “10.3 INSERT 복구 — heap_rv_redo_insert와 heap_rv_mvcc_redo_insert”비 MVCC redo (heap_rv_redo_insert). 페이로드 형식은 [INT16 record_type][레코드 바이트]다:
// heap_rv_redo_insert -- src/storage/heap_file.crecdes.type = *(INT16 *) (rcv->data); /* <- type 접두사; recdes는 그 뒤를 가리킴 */if (recdes.type == REC_ASSIGN_ADDRESS) /* <- 예약 전용 슬롯: data가 바이트 수 */ { recdes.area_size = recdes.length = *(INT16 *) recdes.data; recdes.data = NULL; }sp_success = spage_insert_for_recovery (thread_p, rcv->pgptr, slotid, &recdes); /* fail: fatal er_set */일반 레코드는 바이트를 복사하고, REC_ASSIGN_ADDRESS(챕터 4)는 복사 없이 슬롯만 예약한다. RVHF_INSERT / RVHF_INSERT_NEWHOME의 redo에 사용된다.
MVCC redo (heap_rv_mvcc_redo_insert). insert MVCCID는 페이로드에 없고 rcv->mvcc_id에 있으므로, 핸들러는 가시성 데이터(챕터 3)가 올바르도록 헤더를 재구성한다:
// heap_rv_mvcc_redo_insert -- src/storage/heap_file.c// ... mask vacuum flag, see 10.2 ...if (record_type == REC_BIGONE) /* <- overflow: 인라인 헤더 재구성 불필요 */ { HEAP_SET_RECORD (&recdes, ..., REC_BIGONE, rcv->data + sizeof (record_type)); }else /* 인라인 레코드: 헤더 재구성 */ { MVCC_SET_INSID (&mvcc_rec_header, rcv->mvcc_id); /* <- INSID는 로그 헤더에서, 페이지 아님 */ or_mvcc_add_header (&recdes, &mvcc_rec_header, ...); }spage_insert_for_recovery (thread_p, rcv->pgptr, slotid, &recdes);heap_page_rv_chain_update (thread_p, rcv->pgptr, rcv->mvcc_id, vacuum_status_change); /* 체인 재적용 */두 분기 모두 insert 후 heap_page_rv_chain_update로 페이지 체인 MVCCID와 저장된 vacuum 상태 변경을 재적용한다.
불변 조건 — redo된 MVCC insert는 크래시 이전과 동일한 INSID를 가진다. INSID는 페이지 바이트가 아니라
rcv->mvcc_id에서 가져오므로, redo가 멱등(idempotent)이고 스냅샷 일관성을 유지한다. 오래된 바이트를 사용하면 커밋된 히스토리와 어긋난다.
10.4 INSERT undo — heap_rv_undo_insert는 삭제다
섹션 제목: “10.4 INSERT undo — heap_rv_undo_insert는 삭제다”커밋되지 않은 insert는 롤백 시 사라져야 한다: 슬롯을 삭제하고, 시스템이 완전히 재시작된 이후에만 여유 공간 통계를 복구한다:
// heap_rv_undo_insert -- src/storage/heap_file.cif (LOG_ISRESTARTED ()) /* <- 재시작 이후에만 해제된 공간 측정 */ { free_space = spage_get_free_space_without_saving (thread_p, rcv->pgptr, NULL); }slotid = rcv->offset & (~HEAP_RV_FLAG_VACUUM_STATUS_CHANGE);(void) spage_delete_for_recovery (thread_p, rcv->pgptr, slotid); /* <- 슬롯 재사용 */pgbuf_set_dirty (thread_p, rcv->pgptr, DONT_FREE);if (LOG_ISRESTARTED ()) /* <- HFID 조회(best-effort), 통계 수정 */ { if (heap_get_class_oid_from_page (...) != NO_ERROR || heap_get_class_info (...) != NO_ERROR) goto end; heap_stats_update (thread_p, rcv->pgptr, &hfid, free_space); }end: ; /* NO_ERROR 반환으로 fall-through */크래시 복구 중에는 슬롯 삭제만 수행된다(best-space 통계, 챕터 9는 아직 신뢰할 수 없음). 재시작 이후에는 heap_get_class_* 실패 시 goto end로 오류를 흡수한다(통계는 힌트에 불과). OID는 재사용된다 — 커밋되지 않은 insert는 영구적이지 않았다. RVHF_INSERT, RVHF_MVCC_INSERT, RVHF_INSERT_NEWHOME, RVHF_MVCC_REDISTRIBUTE의 undo에 사용된다.
10.5 UPDATE 복구 — heap_rv_undoredo_update와 prev_version 연결
섹션 제목: “10.5 UPDATE 복구 — heap_rv_undoredo_update와 prev_version 연결”UPDATE 복구는 독특하다: 하나의 함수가 undo와 redo 모두를 처리한다. 어느 방향이든 슬롯 slotid를 페이로드 바이트로 덮어쓰기 때문이다. heap_rv_redo_update는 단순한 one-line 래퍼이고, heap_rv_undo_update는 동일한 코어를 호출한 뒤 vacuum 검사를 추가한다.
// heap_rv_undoredo_update -- src/storage/heap_file.c// ... mask vacuum flag (10.2); assert (slotid > 0); type 접두사 이후로 recdes 이동 ...if (recdes.area_size <= 0) { sp_success = SP_SUCCESS; } /* <- 빈 이미지: 헤더만 변경 */else if (heap_update_physical (thread_p, rcv->pgptr, slotid, &recdes) != NO_ERROR) { assert_release (false); return ER_FAILED; } /* heap_update_physical = spage_update + type 보정 */동일한 페이로드가 양 방향에 작동하는 이유 — prev_version_lsa 링크. 순방향 UPDATE 경로에서 로그를 기록하기 전에, CUBRID는 새 레코드의 헤더에 이전 레코드의 undo 로그 레코드 LSA를 스탬핑한다:
// heap_update_set_prev_version -- src/storage/heap_file.cif (recdes.type == REC_HOME) { or_mvcc_set_log_lsa_to_record (&recdes, prev_version_lsa); } /* <- 이전 레코드 undo 로그의 LSA */따라서 UPDATE의 undo 이미지(이전 바이트)는 새 레코드의 prev_version_lsa가 가리키는 정확한 LSA에 위치한다. 버전 체인 리더(챕터 5)는 거기서 undo 레코드를 가져오는데, 이는 heap_rv_undoredo_update가 롤백 시 재생할 내용과 정확히 같다: 복구와 MVCC 시간 여행이 동일한 물리적 undo 레코드를 읽으며, 두 번째 복사본은 없다. (heap_update_bigone도 overflow에 동일한 링크를 연결한다.)
불변 조건 —
prev_version_lsa는 전임자 바이트를 담은 undo 로그 레코드의 LSA와 같다.heap_update_set_prev_version과heap_update_bigone이 이를 강제한다. 불일치가 발생하면 롤백이 잘못된 바이트를 복원하거나 스냅샷 읽기가 쓰레기 값으로 이동한다 — 크래시가 아니라 조용한 데이터 손상이다.
10.6 MVCC 삭제 redo — heap_rv_redo_update_and_update_chain
섹션 제목: “10.6 MVCC 삭제 redo — heap_rv_redo_update_and_update_chain”MVCC 삭제는 레코드를 지우는 대신 헤더에 delete-MVCCID를 스탬핑한다(챕터 7) — 물리적으로는 업데이트다. 따라서 vacuum 플래그 마스킹(§10.2) 후, heap_rv_redo_update_and_update_chain은 문자 그대로 heap_rv_redo_update (thread_p, rcv)(§10.5 코어) 실행 후 heap_page_rv_chain_update (..., rcv->mvcc_id, vacuum_status_change)를 호출한다. 내부 오류는 ASSERT_ERROR ()로 전파된다. 이 핸들러는 RVHF_MVCC_DELETE_MODIFY_HOME과 RVHF_UPDATE_NOTIFY_VACUUM 모두의 redo에 쓰인다. undo 측인 heap_rv_undo_update는 이전 헤더 바이트를 복원하고 vacuum_rv_check_at_undo(REC_HOME / REC_NEWHOME)를 실행해, 롤백된 삭제가 vacuum에 거짓으로 visible하게 남지 않도록 한다.
10.7 DELETE 복구 — 비 MVCC와 MVCC undo
섹션 제목: “10.7 DELETE 복구 — 비 MVCC와 MVCC undo”비 MVCC 삭제. Redo(heap_rv_redo_delete)는 단순한 spage_delete이고, undo(heap_rv_undo_delete)는 insert의 mirror다:
// heap_rv_undo_delete -- src/storage/heap_file.cerror_code = heap_rv_redo_insert (thread_p, rcv); /* <- 삭제된 레코드 재삽입 */if (error_code != NO_ERROR) { return error_code; }recdes_type = *(INT16 *) (rcv->data);if (recdes_type == REC_NEWHOME) /* <- REC_NEWHOME만 guard 필요 */ { vacuum_rv_check_at_undo (thread_p, rcv->pgptr, slotid, recdes_type); } /* fail: assert_release+ER_FAILED */redo 페이로드에 type과 바이트가 남아 있으므로 재삽입이 가능하다. REC_NEWHOME만 vacuum 원자성 검사를 추가로 실행한다.
MVCC 삭제 undo (heap_rv_mvcc_undo_delete)는 레코드를 삭제하는 대신 delete-MVCCID 플래그를 제거한다:
// heap_rv_mvcc_undo_delete -- src/storage/heap_file.cslotid = rcv->offset & (~HEAP_RV_FLAG_VACUUM_STATUS_CHANGE);spage_get_record (..., slotid, &rebuild_record, COPY); /* 현재 페이지 바이트 읽기 */or_mvcc_get_header (&rebuild_record, &mvcc_rec_header);assert (MVCC_IS_FLAG_SET (&mvcc_rec_header, OR_MVCC_FLAG_VALID_DELID)); /* 삭제되었어야 함 */MVCC_CLEAR_FLAG_BITS (&mvcc_rec_header, OR_MVCC_FLAG_VALID_DELID); /* <- 삭제 취소 후 or_mvcc_set_header */spage_update (..., slotid, &rebuild_record); /* 각 단계 실패 시: assert_release(false)+ER_FAILED */redo 페이로드가 없다 — undo 이미지는 “DELID 플래그를 제거한 페이지”다. RVHF_MVCC_DELETE_REC_HOME / ..._REC_NEWHOME의 undo에 사용된다.
10.8 페이지 수준 복구 — heap_rv_redo_newpage와 heap_rv_redo_reuse_page
섹션 제목: “10.8 페이지 수준 복구 — heap_rv_redo_newpage와 heap_rv_redo_reuse_page”신규 페이지 (heap_rv_redo_newpage)는 페이지의 첫 번째 상태를 redo한다 — type 설정, 복구 공간 예약 활성화 상태로 spage_initialize, 예약 슬롯에 header/chain 레코드 삽입:
// heap_rv_redo_newpage -- src/storage/heap_file.cspage_initialize (thread_p, rcv->pgptr, heap_get_spage_type (), HEAP_MAX_ALIGN, SAFEGUARD_RVSPACE);sp_success = spage_insert (thread_p, rcv->pgptr, &recdes, &slotid); /* recdes.type = REC_HOME */if (sp_success != SP_SUCCESS || slotid != HEAP_HEADER_AND_CHAIN_SLOTID) { er_set (ER_FATAL_ERROR_SEVERITY, ...); return er_errid (); } /* header/chain은 반드시 슬롯 0, 아니면 fatal */페이지 재사용 (heap_rv_redo_reuse_page)은 페이지가 다른 클래스로 재활용될 때 모든 레코드를 일괄 삭제한다:
// heap_rv_redo_reuse_page -- src/storage/heap_file.cconst bool is_header_page = ((rcv->offset != 0) ? true : false);(void) heap_delete_all_page_records (thread_p, &vpid, rcv->pgptr); /* 멱등: redo는 두 번 실행될 수 있음 */if (!is_header_page) /* 헤더 페이지는 RVHF_STATS로 나중에 처리 */ { COPY_OID (&(chain->class_oid), (OID *) (rcv->data)); ... } /* <- class 재스탬핑, max_mvccid 초기화, vacuum=NONE */heap_delete_all_page_records는 redo가 두 번 실행될 수 있으므로 멱등이다. 헤더 페이지는 체인 재작성을 건너뛰며(RVHF_STATS로 나중에 처리), 비 헤더 페이지는 class OID를 재스탬핑하고 max_mvccid를 초기화한다.
10.9 Slotted-page 복구 프리미티브
섹션 제목: “10.9 Slotted-page 복구 프리미티브”이 프리미티브들은 복구를 위해 존재한다: 순방향 spage_insert는 슬롯을 선택하지만, 복구는 OID가 크래시 이후에도 안정적이도록 레코드를 특정, 이전에 할당된 slotid에 배치해야 한다.
// spage_insert_for_recovery -- src/storage/slotted_page.cif (anchor_type != ANCHORED && anchor_type != ANCHORED_DONT_REUSE_SLOTS) { return spage_insert_at (thread_p, page_p, slot_id, record_descriptor_p); } /* unanchored: shift */if (slot_id < page_header_p->num_slots) /* 슬롯 존재: 비어 있음을 assert, 재채움 */ { slot_p->record_type = REC_DELETED_WILL_REUSE; } /* <- 이 slotid에서 OID 유지 */spage_find_empty_slot_at (thread_p, page_p, slot_id, ...);if (record_descriptor_p->type != REC_ASSIGN_ADDRESS) /* <- ASSIGN_ADDRESS는 예약만, 복사 없음 */ { memcpy ((char *) page_p + slot_p->offset_to_record, record_descriptor_p->data, ...); }// spage_delete_for_recovery -- src/storage/slotted_page.cif (spage_delete (thread_p, page_p, slot_id) != slot_id) { return NULL_SLOTID; }if (page_header_p->anchor_type == ANCHORED_DONT_REUSE_SLOTS) /* 일반 삭제는 REC_MARKDELETED 남김 */ { slot_p = spage_find_slot (page_p, page_header_p, slot_id, false); if (slot_p->offset_to_record == SPAGE_EMPTY_OFFSET && slot_p->record_type == REC_MARKDELETED) { slot_p->record_type = REC_DELETED_WILL_REUSE; pgbuf_set_dirty (...); } /* <- no-reuse 재정의 */ }불변 조건 — 복구는 커밋되지 않은 OID를 위해 슬롯을 소모하지 않는다.
spage_delete_for_recovery는ANCHORED_DONT_REUSE_SLOTS페이지에서도 강제로REC_DELETED_WILL_REUSE로 설정한다.REC_MARKDELETED를 남겨두면 롤백된 insert마다 슬롯이 누수되고, 크래시 이후 OID가 불필요하게 팽창한다.
10.10 is_saving / spage_save_space undo 공간 예약
섹션 제목: “10.10 is_saving / spage_save_space undo 공간 예약”SPAGE_HEADER::is_saving(챕터 2; SAFEGUARD_RVSPACE = true로 spage_initialize에서 설정)은 순전히 복구를 위해 존재한다: 트랜잭션이 공간을 해제할 때 나중에 롤백이 레코드를 다시 키울 수 있도록 그 공간을 예약해야 한다. spage_delete는 is_saving일 때만 spage_save_space를 호출하며, 이 함수는 세 가지 경우에 조기 종료된다:
// spage_save_space -- src/storage/slotted_page.cif (space == 0 || log_is_in_crash_recovery ()) { return NO_ERROR; }if (VACUUM_IS_THREAD_VACUUM_WORKER (thread_p)) { return NO_ERROR; } /* vacuum은 롤백하지 않음 */if (space < 0 || !logtb_is_active (thread_p, tranid)) { return NO_ERROR; }// ... 그 외: VPID로 SPAGE_SAVE_HEAD를 find_or_insert, tranid로 SPAGE_SAVE_ENTRY 확장 ...양수 공간을 해제하는 활성 순방향 트랜잭션만 spage_Saving_hashmap에서 VPID를 키로 항목을 기록하고, TDES에 연결해 트랜잭션 종료 시 해제된다(§10.4의 heap_rv_undo_insert가 이 예약값을 읽는다).
불변 조건 — 해제된 공간은 커밋 또는 취소 전까지 해제 트랜잭션을 위해 예약되지만, 복구 중에는 작동하지 않는다.
is_saving게이트와log_is_in_crash_recovery단락 회로(short-circuit)가 이를 강제한다. 그렇지 않으면 hashmap이 죽은 트랜잭션의 유령 항목으로 가득 찬다.
10.11 챕터 요약 — 핵심 정리
섹션 제목: “10.11 챕터 요약 — 핵심 정리”- 복구는 테이블 기반이며 대칭이다.
RV_fun[]은 각RVHF_*를 undo/redo 쌍에 고정한다. undo-insert는 삭제하고, undo-delete는 삽입하며, UPDATE는heap_rv_undoredo_update코어 하나를 양방향으로 공유한다. - vacuum 비트는
rcv->offset에 실려 온다. 핸들러는HEAP_RV_FLAG_VACUUM_STATUS_CHANGE(0x8000)를 마스킹하며, 레코드 재구성 핸들러는assert (slotid > 0)도 수행한다.heap_rv_undo_insert는 마스킹만 한다. - MVCC redo는 로그에서 헤더를 재구성한다. INSID는 페이지 바이트가 아니라
rcv->mvcc_id에서 가져오므로, redo가 멱등이고 스냅샷 일관성을 유지한다. - UPDATE undo 이미지는 곧 버전 체인 전임자다.
heap_update_set_prev_version/heap_update_bigone은 새 레코드의prev_version_lsa에 이전 레코드의 undo 레코드 LSA를 스탬핑하므로, MVCC 읽기(챕터 5)와 롤백이 동일한 물리적 레코드를 재생한다. - 복구에는 자체 slotted-page 프리미티브가 있다.
spage_insert_for_recovery는 OID를 특정 slotid에 안정적으로 유지하고,spage_delete_for_recovery는REC_DELETED_WILL_REUSE를 강제해 롤백된 insert가 슬롯을 누수하지 않도록 한다. is_saving은 롤백을 위해 해제된 공간을 예약하지만, 복구 중에는 비활성화된다.log_is_in_crash_recovery단락 회로가 이를 보장한다.- 이 경로는 상태를 재구성할 뿐, 새 상태를 만들지 않는다 — 모든 핸들러는 챕터 4~9 연산의 기계적 역연산 또는 재생이다.
이 리비전 시점의 위치 힌트
섹션 제목: “이 리비전 시점의 위치 힌트”아래 줄 번호는 2026-06-23 시점에 관찰한 값이다. 심볼 이름이 정식 앵커이며 줄 번호는 시간이 지나면 어긋나는 힌트다.
| Symbol | File | Line |
|---|---|---|
OR_MVCC_DELETE_ID_OFFSET | src/base/object_representation.h | 486 |
OR_MVCC_MAX_HEADER_SIZE | src/base/object_representation_constants.h | 142 |
OR_MVCC_MIN_HEADER_SIZE | src/base/object_representation_constants.h | 145 |
OR_MVCC_FLAG_MASK | src/base/object_representation_constants.h | 160 |
OR_MVCC_FLAG_VALID_INSID | src/base/object_representation_constants.h | 165 |
OR_MVCC_FLAG_VALID_DELID | src/base/object_representation_constants.h | 168 |
OR_MVCC_FLAG_VALID_PREV_VERSION | src/base/object_representation_constants.h | 171 |
OR_MVCC_REPID_MASK | src/base/object_representation_constants.h | 173 |
or_mvcc_get_header | src/base/object_representation_sr.c | 4237 |
or_mvcc_set_header | src/base/object_representation_sr.c | 4296 |
mvcc_header_size_lookup | src/object/object_representation.c | 70 |
or_header_size | src/object/object_representation.c | 5757 |
vacuum_heap_page | src/query/vacuum.c | 1577 |
vacuum_is_mvccid_vacuumed | src/query/vacuum.c | 7463 |
HEAP_BESTSPACE_SYNC_THRESHOLD | src/storage/heap_file.c | 90 |
HEAP_MVCC_SET_HEADER_MAXIMUM_SIZE | src/storage/heap_file.c | 129 |
HEAP_UPDATE_IS_MVCC_OP | src/storage/heap_file.c | 151 |
HEAP_NUM_BEST_SPACESTATS | src/storage/heap_file.c | 182 |
HEAP_STATS_NEXT_BEST_INDEX | src/storage/heap_file.c | 185 |
HEAP_STATS_PREV_BEST_INDEX | src/storage/heap_file.c | 187 |
heap_hdr_stats | src/storage/heap_file.c | 191 |
HEAP_PAGE_FLAG_VACUUM_STATUS_MASK | src/storage/heap_file.c | 240 |
HEAP_PAGE_SET_VACUUM_STATUS | src/storage/heap_file.c | 244 |
HEAP_PAGE_GET_VACUUM_STATUS | src/storage/heap_file.c | 262 |
heap_chain | src/storage/heap_file.c | 270 |
struct heap_chain | src/storage/heap_file.c | 270 |
heap_stats_bestspace_cache | src/storage/heap_file.c | 469 |
heap_Find_best_page_limit | src/storage/heap_file.c | 488 |
heap_Bestspace | src/storage/heap_file.c | 499 |
HEAP_RV_FLAG_VACUUM_STATUS_CHANGE | src/storage/heap_file.c | 514 |
heap_stats_add_bestspace | src/storage/heap_file.c | 1024 |
heap_is_big_length | src/storage/heap_file.c | 1330 |
heap_get_spage_type | src/storage/heap_file.c | 1353 |
heap_is_reusable_oid | src/storage/heap_file.c | 1364 |
heap_stats_get_min_freespace | src/storage/heap_file.c | 2917 |
heap_stats_update | src/storage/heap_file.c | 2966 |
heap_stats_update_internal | src/storage/heap_file.c | 3020 |
heap_stats_put_second_best | src/storage/heap_file.c | 3142 |
heap_stats_get_second_best | src/storage/heap_file.c | 3184 |
heap_stats_find_page_in_bestspace | src/storage/heap_file.c | 3272 |
heap_stats_find_best_page | src/storage/heap_file.c | 3519 |
heap_stats_sync_bestspace | src/storage/heap_file.c | 3728 |
heap_vpid_alloc | src/storage/heap_file.c | 4284 |
heap_remove_page_on_vacuum | src/storage/heap_file.c | 4698 |
heap_vpid_next | src/storage/heap_file.c | 5038 |
heap_assign_address | src/storage/heap_file.c | 6015 |
xheap_reclaim_addresses | src/storage/heap_file.c | 6227 |
heap_ovf_insert | src/storage/heap_file.c | 6569 |
heap_ovf_update | src/storage/heap_file.c | 6597 |
heap_get_if_diff_chn | src/storage/heap_file.c | 7400 |
heap_prepare_get_context | src/storage/heap_file.c | 7512 |
heap_get_mvcc_header | src/storage/heap_file.c | 7747 |
heap_get_record_data_when_all_ready | src/storage/heap_file.c | 7834 |
heap_next_internal | src/storage/heap_file.c | 7902 |
heap_rv_redo_newpage | src/storage/heap_file.c | 16203 |
heap_rv_redo_insert | src/storage/heap_file.c | 16321 |
heap_mvcc_log_insert | src/storage/heap_file.c | 16371 |
heap_rv_mvcc_redo_insert | src/storage/heap_file.c | 16442 |
heap_rv_undo_insert | src/storage/heap_file.c | 16536 |
heap_rv_redo_delete | src/storage/heap_file.c | 16589 |
heap_mvcc_log_delete | src/storage/heap_file.c | 16610 |
heap_rv_mvcc_undo_delete | src/storage/heap_file.c | 16663 |
heap_rv_redo_mark_reusable_slot | src/storage/heap_file.c | 16929 |
heap_rv_undo_delete | src/storage/heap_file.c | 16946 |
heap_rv_undo_update | src/storage/heap_file.c | 16981 |
heap_rv_redo_update | src/storage/heap_file.c | 17018 |
heap_rv_undoredo_update | src/storage/heap_file.c | 17029 |
heap_rv_redo_reuse_page | src/storage/heap_file.c | 17065 |
heap_next | src/storage/heap_file.c | 19427 |
heap_get_mvcc_rec_header_from_overflow | src/storage/heap_file.c | 19540 |
heap_set_mvcc_rec_header_on_overflow | src/storage/heap_file.c | 19566 |
heap_set_mvcc_rec_header_on_overflow | src/storage/heap_file.c | 19567 |
heap_get_bigone_content | src/storage/heap_file.c | 19610 |
heap_mvcc_log_home_change_on_delete | src/storage/heap_file.c | 19689 |
heap_mvcc_log_home_no_change | src/storage/heap_file.c | 19724 |
heap_rv_redo_update_and_update_chain | src/storage/heap_file.c | 19745 |
heap_clear_operation_context | src/storage/heap_file.c | 20231 |
heap_build_forwarding_recdes | src/storage/heap_file.c | 20516 |
heap_insert_adjust_recdes_header | src/storage/heap_file.c | 20539 |
heap_insert_adjust_recdes_header | src/storage/heap_file.c | 20540 |
heap_update_adjust_recdes_header | src/storage/heap_file.c | 20671 |
heap_insert_handle_multipage_record | src/storage/heap_file.c | 20834 |
heap_get_insert_location_with_lock | src/storage/heap_file.c | 20885 |
heap_find_location_and_insert_rec_newhome | src/storage/heap_file.c | 21022 |
heap_insert_newhome | src/storage/heap_file.c | 21105 |
heap_insert_physical | src/storage/heap_file.c | 21169 |
heap_log_insert_physical | src/storage/heap_file.c | 21229 |
heap_delete_adjust_header | src/storage/heap_file.c | 21290 |
heap_delete_bigone | src/storage/heap_file.c | 21389 |
heap_delete_relocation | src/storage/heap_file.c | 21570 |
heap_delete_home | src/storage/heap_file.c | 22067 |
heap_delete_physical | src/storage/heap_file.c | 22388 |
heap_log_delete_physical | src/storage/heap_file.c | 22428 |
heap_update_bigone | src/storage/heap_file.c | 22484 |
heap_update_relocation | src/storage/heap_file.c | 22700 |
heap_update_home | src/storage/heap_file.c | 23026 |
heap_update_physical | src/storage/heap_file.c | 23257 |
heap_create_insert_context | src/storage/heap_file.c | 23358 |
heap_create_delete_context | src/storage/heap_file.c | 23385 |
heap_create_update_context | src/storage/heap_file.c | 23412 |
heap_insert_logical | src/storage/heap_file.c | 23460 |
heap_delete_logical | src/storage/heap_file.c | 23676 |
heap_update_logical | src/storage/heap_file.c | 23867 |
heap_vacuum_all_objects | src/storage/heap_file.c | 24408 |
heap_page_update_chain_after_mvcc_op | src/storage/heap_file.c | 24785 |
heap_page_set_vacuum_status_none | src/storage/heap_file.c | 24939 |
heap_page_get_vacuum_status | src/storage/heap_file.c | 25014 |
heap_get_visible_version_from_log | src/storage/heap_file.c | 25329 |
heap_get_visible_version | src/storage/heap_file.c | 25456 |
heap_scan_get_visible_version | src/storage/heap_file.c | 25494 |
heap_get_visible_version_internal | src/storage/heap_file.c | 25577 |
heap_update_set_prev_version | src/storage/heap_file.c | 25689 |
heap_get_last_version | src/storage/heap_file.c | 25793 |
heap_prepare_object_page | src/storage/heap_file.c | 25856 |
heap_clean_get_context | src/storage/heap_file.c | 25904 |
heap_init_get_context | src/storage/heap_file.c | 25944 |
heap_alloc_new_page | src/storage/heap_file.c | 26241 |
HEAP_HEADER_AND_CHAIN_SLOTID | src/storage/heap_file.h | 62 |
HEAP_ISJUNK_OID | src/storage/heap_file.h | 66 |
HEAP_SCANCACHE_SET_NODE | src/storage/heap_file.h | 83 |
HEAP_DROP_FREE_SPACE | src/storage/heap_file.h | 103 |
heap_bestspace | src/storage/heap_file.h | 120 |
heap_scancache_node | src/storage/heap_file.h | 127 |
heap_scancache | src/storage/heap_file.h | 143 |
HEAP_OPERATION_TYPE | src/storage/heap_file.h | 251 |
update_inplace_style | src/storage/heap_file.h | 253 |
HEAP_IS_UPDATE_INPLACE | src/storage/heap_file.h | 262 |
heap_operation_context | src/storage/heap_file.h | 267 |
HEAP_PAGE_VACUUM_STATUS | src/storage/heap_file.h | 354 |
heap_get_context | src/storage/heap_file.h | 362 |
spage_verify_header | src/storage/slotted_page.c | 346 |
spage_is_valid_anchor_type | src/storage/slotted_page.c | 375 |
spage_free_saved_spaces | src/storage/slotted_page.c | 393 |
spage_save_space | src/storage/slotted_page.c | 488 |
spage_initialize | src/storage/slotted_page.c | 1094 |
spage_compact | src/storage/slotted_page.c | 1174 |
spage_find_free_slot | src/storage/slotted_page.c | 1294 |
spage_check_space | src/storage/slotted_page.c | 1347 |
spage_find_empty_slot | src/storage/slotted_page.c | 1396 |
spage_add_new_slot | src/storage/slotted_page.c | 1568 |
spage_take_slot_in_use | src/storage/slotted_page.c | 1608 |
spage_find_empty_slot_at | src/storage/slotted_page.c | 1674 |
spage_check_record_for_insert | src/storage/slotted_page.c | 1745 |
spage_insert | src/storage/slotted_page.c | 1769 |
spage_find_slot_for_insert | src/storage/slotted_page.c | 1801 |
spage_insert_data | src/storage/slotted_page.c | 1841 |
spage_insert_at | src/storage/slotted_page.c | 1902 |
spage_insert_for_recovery | src/storage/slotted_page.c | 1962 |
spage_is_record_located_at_end | src/storage/slotted_page.c | 2039 |
spage_reduce_a_slot | src/storage/slotted_page.c | 2057 |
spage_delete | src/storage/slotted_page.c | 2084 |
spage_delete_for_recovery | src/storage/slotted_page.c | 2177 |
spage_check_updatable | src/storage/slotted_page.c | 2223 |
spage_update_record_in_place | src/storage/slotted_page.c | 2409 |
spage_update_record_after_compact | src/storage/slotted_page.c | 2465 |
spage_update | src/storage/slotted_page.c | 2556 |
spage_reclaim | src/storage/slotted_page.c | 2719 |
spage_mark_deleted_slot_as_reusable | src/storage/slotted_page.c | 4022 |
spage_find_slot | src/storage/slotted_page.c | 4609 |
spage_has_enough_total_space | src/storage/slotted_page.c | 4639 |
spage_has_enough_contiguous_space | src/storage/slotted_page.c | 4679 |
spage_vacuum_slot | src/storage/slotted_page.c | 4857 |
spage_need_compact | src/storage/slotted_page.c | 5275 |
ANCHORED | src/storage/slotted_page.h | 38 |
SP_ERROR | src/storage/slotted_page.h | 49 |
SP_SUCCESS | src/storage/slotted_page.h | 50 |
SP_DOESNT_FIT | src/storage/slotted_page.h | 51 |
SAFEGUARD_RVSPACE | src/storage/slotted_page.h | 53 |
SPAGE_HEADER_FLAG_NONE | src/storage/slotted_page.h | 57 |
spage_header | src/storage/slotted_page.h | 64 |
spage_slot | src/storage/slotted_page.h | 88 |
hfid | src/storage/storage_common.h | 193 |
record_type | src/storage/storage_common.h | 1145 |
REC_UNKNOWN | src/storage/storage_common.h | 1148 |
REC_ASSIGN_ADDRESS | src/storage/storage_common.h | 1151 |
REC_HOME | src/storage/storage_common.h | 1154 |
REC_NEWHOME | src/storage/storage_common.h | 1157 |
REC_RELOCATION | src/storage/storage_common.h | 1160 |
REC_BIGONE | src/storage/storage_common.h | 1163 |
REC_MARKDELETED | src/storage/storage_common.h | 1168 |
REC_DELETED_WILL_REUSE | src/storage/storage_common.h | 1173 |
REC_4BIT_USED_TYPE_MAX | src/storage/storage_common.h | 1185 |
mvcc_rec_header | src/transaction/mvcc.h | 38 |
MVCC_REC_HEADER_INITIALIZER | src/transaction/mvcc.h | 47 |
MVCC_IS_REC_DELETED_BY | src/transaction/mvcc.h | 130 |
MVCC_IS_CHN_UPTODATE | src/transaction/mvcc.h | 137 |
RVHF_INSERT | src/transaction/recovery.c | 279 |
cubrid-heap-manager.md— 설계 의도와 이론적 배경을 다루는 상위 동반 문서.raw/code-analysis/cubrid/storage/heap_manager/하위 원시 분석 자료.- 코드:
src/storage/heap_file.{c,h},src/storage/slotted_page.{c,h}. - 방법론:
knowledge/methodology/code-analysis-detail-doc.md.