CUBRID Overflow File — 코드 수준 심층 분석
이 문서의 위치: 상위 분석 문서인
cubrid-overflow-file.md는 설계 의도와 이론적 배경을 다룬다. 이 문서는 모든 분기와 필드를 코드 수준에서 추적한다. 각 장은 독립적으로 읽을 수 있지만, 순서대로 읽으면 하나의 초대형 레코드가 첫 overflow 페이지 체인에서 갱신·읽기·회수에 이르는 흐름 전체를 따라갈 수 있다.
목차:
| Ch | 제목 | 상태 |
|---|---|---|
| 1 | 자료 구조 전체 지도 | ✅ |
| 2 | 파일 프로비저닝과 소유 참조 | ✅ |
| 3 | overflow_insert로 체인 만들기 | ✅ |
| 4 | 체인 읽기와 순회 | ✅ |
| 5 | overflow_update로 체인을 제자리에서 갱신하기 | ✅ |
| 6 | 체인 삭제와 페이지 반환 | ✅ |
| 7 | B+Tree OID 리스트 변형 구현 | ✅ |
| 8 | 크래시 복구와 예외 경로 | ✅ |
Chapter 1: 자료 구조 전체 지도
섹션 제목: “Chapter 1: 자료 구조 전체 지도”overflow 파일 관리자(src/storage/overflow_file.c / .h)는 한 페이지에 담을 수 없는 단일 논리 레코드를 단일 연결 페이지 체인(private singly-linked chain) 으로 저장한다. 모든 진입점은 고정 크기 페이지 버퍼(PAGE_PTR, DB_PAGESIZE 바이트)를 두 C 구조체 중 하나로 재해석하며, 구조체 선택은 오직 체인 내 위치 로만 결정된다. 첫 번째 페이지는 overflow_first_part, 그 외 모든 페이지는 overflow_rest_part다. 이 모듈에는 별도의 온-디스크 페이지 헤더 구조체, 슬롯 디렉터리, 체크섬 필드가 없다 — 페이지 버퍼 포인터를 직접 구조체 포인터로 캐스트하여 사용한다.
이 챕터는 한 가지 질문에 답한다: 각 overflow 페이지에는 무엇이 있으며, 두 포맷은 필드 수준에서 어떻게 다른가? DBMS가 overflow 페이지, 첫 페이지의 길이 필드, 잠금 없는 전용 체인을 왜 사용하는지 하는 배경 설명은 이 문서가 아닌 고수준 동반 문서(Common DBMS Design)에서 다룬다.
1.1 두 페이지 포맷 — 필드별 비교
섹션 제목: “1.1 두 페이지 포맷 — 필드별 비교”두 구조체는 src/storage/overflow_file.h에 선언되어 있으며, 전체를 인용할 만큼 짧다:
// overflow_first_part -- src/storage/overflow_file.hstruct overflow_first_part{ VPID next_vpid; int length; char data[1]; /* Really more than one */};
// overflow_rest_part -- src/storage/overflow_file.hstruct overflow_rest_part{ VPID next_vpid; char data[1]; /* Really more than one */};VPID(src/storage/storage_common.h의 { PAGEID pageid; VOLID volid; } 쌍)는 온-디스크 페이지 주소, 즉 연결 리스트의 “다음 포인터”다. data[1]은 C의 trailing-array 관용구다 — 원소 하나로 선언하여 offsetof(...,data)가 명확히 정의되도록 하되, 실제로는 페이지 끝까지 data가 이어진다. 주석 /* Really more than one */이 이를 명시한다. 코드는 data[0] 이후 원소를 인덱스로 접근하지 않고, (char *) ...->data를 취해 memcpy로 일정 바이트를 복사한다.
overflow_first_part:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
next_vpid | 이 체인의 두 번째 페이지 VPID; 레코드가 한 페이지에 모두 들어가면 NULL-VPID. | 연결 리스트 포인터. 나머지 레코드로 가는 유일한 경로. |
length | 전체 논리 레코드의 바이트 길이(모든 페이지 합산). 이 페이지의 바이트 수가 아님. | 체인에 내용 종료 센티널이 없으므로, 독자는 출력 버퍼 크기 결정과 복사 중단 시점을 위해 전체 길이를 미리 알아야 한다. 동반 문서 ### Length stored on the first page 참고. |
data[1] | 페이로드 바이트 스트림의 시작; 레코드의 첫 DB_PAGESIZE - offsetof(...,data) 바이트가 여기에 있다. | 실제 레코드 내용. [1]은 trailing-array 관용구; 실제 범위는 페이지 끝까지다. |
overflow_rest_part:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
next_vpid | 다음 페이지의 VPID; 체인의 마지막 페이지면 NULL-VPID. | 연결 리스트 포인터. NULL이 체인 종료 표시다. |
data[1] | 이전 페이지 data가 끝난 곳부터 이어지는 페이로드 바이트 스트림. | 추가 레코드 내용. length 없음 — 아래 참고. |
1.2 첫 번째 페이지에만 length가 있는 이유
섹션 제목: “1.2 첫 번째 페이지에만 length가 있는 이유”length는 페이지당 수량이 아니라 레코드 단위 수량이므로, 외부에서 주소를 아는 유일한 페이지인 헤드 VPID에 딱 한 번 저장한다. 모든 독자는 헤드를 먼저 픽스(fix)한 뒤 length를 읽고 나서 체인을 순회한다 — overflow_get_length는 헤드를 OVERFLOW_FIRST_PART *로 캐스트하여 ->length를 직접 반환하며, rest_part에는 접근하지 않는다. rest_part가 길이를 보고할 필요는 없다: 순회는 VPID_ISNULL(&next_vpid)에서 종료되고, 바이트 카운트는 헤드의 length로 이미 한정되어 있기 때문이다. 페이지마다 length를 두면 sizeof(int) 공간이 낭비되고 중복 상태 일관성 부담이 생긴다. 동반 문서 ### Length stored on the first page 참고.
1.3 헤더 크기 차이와 페이지당 용량
섹션 제목: “1.3 헤더 크기 차이와 페이지당 용량”overflow_rest_part는 int length 필드를 생략하므로 헤더가 첫 페이지보다 sizeof(int)(4바이트) 작다. 코드는 헤더 크기를 절대로 하드코딩하지 않고, DB_PAGESIZE - offsetof(...)로 페이지당 페이로드 용량을 유도한다:
- 첫 번째 페이지:
DB_PAGESIZE - offsetof(OVERFLOW_FIRST_PART, data)=DB_PAGESIZE - (sizeof(VPID) + sizeof(int)). - 나머지 페이지:
DB_PAGESIZE - offsetof(OVERFLOW_REST_PART, data)=DB_PAGESIZE - sizeof(VPID).
따라서 나머지 페이지는 첫 번째 페이지보다 sizeof(int)(4바이트) 더 많은 페이로드를 담는다. 같은 비대칭성이 overflow_insert의 페이지 수 추정을 이끈다 — 첫 페이지의 더 작은 용량을 레코드에서 먼저 차감하고, 남은 바이트를 더 큰 나머지 페이지 용량으로 나눈다:
// overflow_insert -- src/storage/overflow_file.clength = recdes->length - (DB_PAGESIZE - (int) offsetof (OVERFLOW_FIRST_PART, data));if (length > 0) { i = DB_PAGESIZE - offsetof (OVERFLOW_REST_PART, data); npages = 1 + CEIL_PTVDIV (length, i); }else { npages = 1; }여기서 빼기 후의 length는 헤드 이후의 잔여 바이트 수이고, i는 나머지 페이지 용량, npages는 헤드 1개에 ceil(잔여 / i)를 더한 값(레코드가 한 페이지에 들어가면 1)이다.
불변 조건 — 헤더 크기는 유도하며, 리터럴을 쓰지 않는다. 모든 페이로드 용량·페이지 수 계산은
offsetof(OVERFLOW_FIRST_PART, data)또는offsetof(OVERFLOW_REST_PART, data)를 읽으며, 상수를 직접 쓰지 않는다 — 이렇게 해야 정렬이나 필드 변경이 있어도 4바이트 비대칭이 올바르게 유지된다.offsetof를 하드코딩된 숫자로 교체하면 페이지 수 추정이 부족해질 수 있다: 복사 루프 끝의assert (length == 0)이 디버그 빌드에서 발화하거나, 릴리스 빌드에서 레코드가 조용히 잘린다.
1.4 체인 불변 조건과 페이지가 잠기지 않는 이유
섹션 제목: “1.4 체인 불변 조건과 페이지가 잠기지 않는 이유”구조적 계약:
불변 조건 — 헤드 VPID가 유일한 외부 핸들이며,
data[]는 페이지에 걸쳐 이어지는 하나의 연속 바이트 스트림이고, 체인은 전용(private)이다. 포함 레코드(힙 relocation-overflow 레코드 또는 B+트리 리프/overflow OID 슬롯)에 저장되는 주소는 오직ovf_vpid, 즉 헤드 페이지뿐이다. 다른 모든 페이지는 헤드에서next_vpid를 따라가야만 도달할 수 있으며, 두 논리 레코드가 한 체인을 공유하지 않는다. 체인이 전용이고 이미 잠긴 포함 레코드를 거쳐야만 도달 가능하므로, overflow 페이지 자체는 잠기지 않는다 — 물리적 일관성을 위해 페이지 버퍼 latch(pgbuf_fix)로 픽스할 뿐, lock manager로 관리하지 않는다.overflow_insert헤더 주석은 이를 명시한다(“Overflow pages are not locked … since they are not shared … and its address is only know by accessing the relocation overflow record data which has been appropriately locked”). 동반 문서### Pages not individually locked참고. 이 규칙이 위반되어 두 레코드가 같은 체인을 가리킨다면, 하나를 삭제할 때 다른 쪽이 아직 참조 중인 페이지가 해제되어 생존 레코드가 손상된다.
“연속 바이트 스트림” 부분은 두 구조체를 상호 운용 가능하게 만든다: 독자는 바이트 k가 first_part에 있고 바이트 k+1이 rest_part에 있다는 사실을 신경 쓰지 않아도 된다. overflow_get_nbytes는 copyfrom을 현재 페이지 data 끝까지 진행시키고, next_vpid를 픽스한 뒤, copyfrom을 새 페이지의 data로 재설정한다 — 두 구조체 모두 data[]로 끝나므로 이음새가 없다.
두 구조체와 체인의 관계:
flowchart LR
ref["포함 레코드\n헤드 VPID만 보유"] --> H
subgraph chain["단일 전용 overflow 체인"]
H["overflow_first_part\nnext_vpid -> P1\nlength = 전체 바이트 수\ndata[ ...페이로드... ]"]
P1["overflow_rest_part\nnext_vpid -> P2\ndata[ ...페이로드... ]"]
P2["overflow_rest_part\nnext_vpid = NULL-VPID\ndata[ ...페이로드... ]"]
end
H --> P1 --> P2
Figure 1-1. 구조체 관계: length를 가진 overflow_first_part 헤드 하나, 그 뒤에 0개 이상의 overflow_rest_part 페이지, 마지막은 NULL next_vpid로 종료. 외부에서 아는 주소는 헤드 VPID뿐이다.
1.5 overflow_next_vpid — 위치가 포맷을 결정하는 방식
섹션 제목: “1.5 overflow_next_vpid — 위치가 포맷을 결정하는 방식”페이지 자체에는 first part인지 rest part인지를 알려주는 플래그가 없다; 위치 가 결정한다 — 이 페이지의 VPID가 헤드 VPID와 같은가? overflow_next_vpid는 이 분기를 수행하는 유일한 헬퍼다:
// overflow_next_vpid -- src/storage/overflow_file.cif (VPID_EQ (ovf_vpid, vpid)) { *vpid = ((OVERFLOW_FIRST_PART *) pgptr)->next_vpid; }else { *vpid = ((OVERFLOW_REST_PART *) pgptr)->next_vpid; }VPID_EQ (ovf_vpid, vpid) 검사가 head/rest 판별자다: 페이지 자신의 VPID가 헤드 VPID와 같으면 OVERFLOW_FIRST_PART *로 캐스트(length가 data 앞에 있는 포맷), 그렇지 않으면 OVERFLOW_REST_PART *로 캐스트한다. 두 구조체 모두 next_vpid를 첫 번째 필드로 배치하므로 링크 만 읽을 때는 어느 캐스트든 올바르다 — 하지만 length나 data 같이 링크 이후 필드에 접근할 때는 올바른 캐스트가 필요하며, 그래서 판별자가 존재한다. data/length를 건드리는 독자들은 이 헬퍼를 호출하는 대신 동일한 VPID_EQ(addr_vpid_ptr, ovf_vpid) 검사를 직접 재현한다.
1.6 OVERFLOW_DO_FUNC — 순회 분기자
섹션 제목: “1.6 OVERFLOW_DO_FUNC — 순회 분기자”체인의 각 페이지에 부수 효과를 적용하는 작업은 overflow_traverse에 집중되어 있으며, enum으로 매개변수화하여 하나의 워커가 전체 체인 작업 두 가지를 모두 처리한다:
// OVERFLOW_DO_FUNC -- src/storage/overflow_file.ctypedef enum{ OVERFLOW_DO_DELETE, OVERFLOW_DO_FLUSH} OVERFLOW_DO_FUNC;두 멤버는 페이지당 부수 효과를 나타낸다: OVERFLOW_DO_DELETE는 방문할 때마다 각 페이지를 해제하는 파괴적 워크(overflow_delete 뒤의 동작), OVERFLOW_DO_FLUSH는 각 페이지를 WAL-flush하는 읽기 전용 워크(overflow_flush 뒤의 동작)이다. overflow_traverse는 페이지마다 이를 스위치한다: OVERFLOW_DO_DELETE는 먼저 ovf_vfid != NULL을 확인하고(if (ovf_vfid == NULL) goto exit_on_error;, 파일 ID 없이는 페이지를 해제할 수 없으므로) overflow_delete_internal을 호출한다; OVERFLOW_DO_FLUSH는 overflow_flush_internal을 호출하고; default: 분기는 break no-op이다. 호출자는 인수 하나만 다르다 — overflow_delete는 실제 ovf_vfid를, overflow_flush는 NULL을 전달 — 따라서 enum 하나로 두 워크를 구분한다. 각 분기의 상세 동작은 챕터 6에서 다룬다.
1.7 file_type 단언과 페이지 타입 태그
섹션 제목: “1.7 file_type 단언과 페이지 타입 태그”이 서브시스템을 서로 다른 레이어에서 보호하는 두 개의 독립적인 타입 태그가 있다; 혼동하지 말 것.
파일 타입 — FILE_TYPE(src/storage/file_manager.h에서 정의). overflow_insert는 세 가지 파일 종류 중 하나 안에서만 체인을 허용하며, 단언으로 강제한다:
// overflow_insert -- src/storage/overflow_file.cassert (file_type == FILE_TEMP /* sort files */ || file_type == FILE_BTREE_OVERFLOW_KEY /* b-tree overflow key */ || file_type == FILE_MULTIPAGE_OBJECT_HEAP /* heap overflow file */ );FILE_TYPE 값 | 사용처 | overflow_insert의 로깅 동작 |
|---|---|---|
FILE_MULTIPAGE_OBJECT_HEAP | 힙 페이지에 담기 너무 큰 힙 레코드. | 로깅됨 (각 새 페이지의 redo + 헤드에 LOG_DUMMY_OVF_RECORD). |
FILE_BTREE_OVERFLOW_KEY | 리프에 인라인하기 너무 큰 B+트리 키. | 로깅됨, 힙과 동일. |
FILE_TEMP | 정렬 / 쿼리 임시 스필 파일. | 로깅 안 함; 할당에 file_init_temp_page_type 사용. file_type != FILE_TEMP 가드가 모든 log_append_* 호출을 건너뜀. |
overflow_update는 이를 file_type == FILE_MULTIPAGE_OBJECT_HEAP만으로 좁히며, 소스 주석은 다른 두 타입에 대해서는 감사되지 않았다고 경고한다(챕터 5).
페이지 타입 — PAGE_OVERFLOW(src/storage/storage_common.h의 PAGE_TYPE enum에서 정의). 파일 종류에 관계없이, 모든 체인 페이지는 페이지 버퍼에서 PAGE_OVERFLOW로 태깅된다. overflow_insert는 PAGE_TYPE ptype = PAGE_OVERFLOW;를 설정하고 &ptype을 file_alloc_multiple에 전달한다; 다른 모든 진입점은 페이지를 픽스한 후 태그를 단언한다:
// repeated guard across overflow_file.c#if !defined (NDEBUG) (void) pgbuf_check_page_ptype (thread_p, addr.pgptr, PAGE_OVERFLOW);#endif /* !NDEBUG */불변 조건 — 픽스된 모든 overflow 페이지는
PAGE_OVERFLOW다. insert가 할당 시 스탬프를 찍고; 복구도 재스탬프한다(overflow_rv_page_update_redo는 내용을 재적용하기 전에pgbuf_set_page_ptype(..., PAGE_OVERFLOW)호출); 모든 읽기·수정·삭제·flush 경로는pgbuf_check_page_ptype으로 디버그 단언한다.next_vpid로 도달한 페이지가 overflow 페이지가 아니면 디버그 빌드에서 단언이 발화한다 — 체인이 손상된 것이다.FILE_TYPE과PAGE_TYPE은 직교한다: 세 가지 허용 파일 타입 모두PAGE_OVERFLOW페이지를 생성한다.
1.8 한 체인의 바이트 수준 레이아웃
섹션 제목: “1.8 한 체인의 바이트 수준 레이아웃”세 페이지에 걸친 레코드의 바이트 맵. V = sizeof(VPID), L = sizeof(int), P = DB_PAGESIZE.
head page (overflow_first_part) offset +-----------------------------------------+ | next_vpid : VPID -> page 2 | 0 | length : int = TOTAL record bytes | V | data[] : payload 0 .. P-V-L-1 | V+L = offsetof(FIRST,data) +-----------------------------------------+ P | next_vpid v rest page 2 (overflow_rest_part) +-----------------------------------------+ | next_vpid : VPID -> page 3 | 0 | data[] : next P-V bytes (L more room)| V = offsetof(REST,data) +-----------------------------------------+ P | next_vpid v rest page 3 (overflow_rest_part) [LAST] +-----------------------------------------+ | next_vpid : NULL-VPID (VPID_ISNULL) | 0 <- chain terminator | data[] : final bytes; tail = slack | V +-----------------------------------------+ P
sum of all three data[] runs == head.length (read-stop condition)Figure 1-2. 세 페이지 체인의 포인터/레이아웃 맵. 헤드는 V+L을 예약하고; 각 나머지 페이지는 V만 예약하여 L(=4바이트) 더 많은 페이로드 공간을 확보한다. 마지막 페이지의 next_vpid는 NULL이며 data[] 끝부분이 미사용일 수 있다 — 레코드 경계는 페이지 충만도가 아니라 헤드의 length가 결정한다.
마지막 페이지의 미사용 끝부분은 실제 여유 공간이다: overflow_get_capacity가 이를 *ovf_free_space로 보고하며, overflow_update는 새 내용이 기존 페이지 범위에 여전히 들어맞을 때 이를 활용해 레코드를 제자리에서 늘린다(챕터 5).
1.9 챕터 요약 — 핵심 정리
섹션 제목: “1.9 챕터 요약 — 핵심 정리”- 두 포맷, 위치로 선택. 페이지의 VPID가 외부에서 알려진 헤드 VPID와 같으면
overflow_first_part, 그렇지 않으면overflow_rest_part다. 페이지 자체에 플래그 없음 —overflow_next_vpid의VPID_EQ검사가 판별자다. length는 헤드에만 있으며, 페이지별 바이트 수가 아닌 전체 레코드 바이트 수를 의미한다. 독자(overflow_get_length,overflow_get_nbytes,overflow_get_capacity)는 순회 전에 헤드에서 한 번 읽는다.- 나머지 페이지는 헤드보다 4바이트 더 많은 페이로드를 담는다 —
int length생략으로 인한 차이이며,sizeof(int)=offsetof(FIRST,data) - offsetof(REST,data)다. 모든 용량 계산은offsetof를 사용하며 리터럴을 쓰지 않는다 — 강제된 불변 조건이다. data[1]은 trailing-array 관용구: 선언된 길이는 1이지만 페이지 끝까지 확장된다; 코드는(char *) ...->data와memcpy를 사용하며,i > 0인data[i]를 인덱스로 쓰지 않는다.- 체인은 전용이며 잠기지 않는다. 헤드 VPID가 유일한 외부 핸들이고; 다른 페이지는
next_vpid링크로만 도달 가능하며; 어떤 페이지도 공유되지 않는다; 페이지는 페이지 버퍼 latch를 받을 뿐 lock manager의 잠금을 받지 않는다. OVERFLOW_DO_FUNC(OVERFLOW_DO_DELETE/OVERFLOW_DO_FLUSH) 는 공유overflow_traverse워커를 파괴적 또는 읽기 전용 워크로 전환하는 단일 분기자다.- 두 개의 직교하는 타입 태그가 서브시스템을 보호한다:
FILE_TYPE(FILE_TEMP,FILE_BTREE_OVERFLOW_KEY,FILE_MULTIPAGE_OBJECT_HEAP중 하나이며 insert의 로깅 여부도 결정) 과PAGE_OVERFLOW(모든 파일 타입의 모든 overflow 페이지에 적용되며 픽스할 때마다 단언됨).
Chapter 2: 파일 프로비저닝과 소유 참조
섹션 제목: “Chapter 2: 파일 프로비저닝과 소유 참조”체인이 존재하려면 두 가지가 먼저 갖춰져야 한다: 페이지를 할당할 파일(VFID)과,
홈 오브젝트가 헤드 페이지를 가리키는 참조. 이 챕터는 파일이 어디서 오는지와
헤드 VPID를 어느 필드가 보유하는지를 추적한다 — 힙 big-record 경로와 B+Tree의 두
경로별로, 파일이 생성되고 참조 슬롯이 준비되는 시점까지만 다룬다(바이트 수준의
삽입 루프는 3챕터). 상위 개요 문서(cubrid-overflow-file.md)는 왜 힙마다, B+Tree마다
파일이 하나인지, 왜 페이지를 개별적으로 잠그지 않는지를 설명하며, 이 챕터는 분기
레벨에서 동작 흐름을 다룬다.
2.1 두 가지 프로비저닝 모델 — lazy vs. lazy-with-preallocation
섹션 제목: “2.1 두 가지 프로비저닝 모델 — lazy vs. lazy-with-preallocation”두 모델 모두 overflow 파일을 지연 생성(lazy) 방식으로 만든다 — 첫 번째 레코드가 흘러 넘치기 전까지는 파일이 존재하지 않는다. 차이점은 이 챕터에서 아래와 같이 분석한다:
| 항목 | 힙 (heap_ovf_find_vfid) | B+Tree (btree_create_overflow_key_file) |
|---|---|---|
| 파일 타입 | FILE_MULTIPAGE_OBJECT_HEAP | FILE_BTREE_OVERFLOW_KEY |
| 사전 할당 페이지 수 | 1 | 최소 3 |
| VFID 저장 위치 | HEAP_HDR_STATS.ovf_vfid (헤더 슬롯 HEAP_HEADER_AND_CHAIN_SLOTID) | BTID_INT::ovfid + 루트 헤더 ovfid |
| 복구 레코드 | RVHF_STATS (undo + redo) | RVBT_UPDATE_OVFID (undoredo) |
| 디스크립터 arm | FILE_OVF_HEAP_DES | FILE_OVF_BTREE_DES |
| 생성자 vs. 탐색 전용 | heap_ovf_insert (docreate=true); heap_ovf_update/heap_ovf_delete (docreate=false) | 키 overflow 시 leaf-insert 경로; find 함수 없음 — BTID_INT::ovfid에서 직접 존재 여부 확인 |
힙의 모든 big record는 힙의 단일 FILE_MULTIPAGE_OBJECT_HEAP을 공유하며,
B+Tree의 두 가지 overflow 용도는 두 개의 별도 파일로 분리된다(§2.5).
flowchart LR HHDR["HEAP_HDR_STATS.ovf_vfid"] -- names --> HFILE["FILE_MULTIPAGE_OBJECT_HEAP\ndesc FILE_OVF_HEAP_DES"] BROOT["Root header ovfid"] -- persisted --> BINT["BTID_INT::ovfid\nin-mem mirror"] BINT -- names --> BFILE["FILE_BTREE_OVERFLOW_KEY\ndesc FILE_OVF_BTREE_DES"]
Figure 2-1 — 소유 파일 참조 구조. 힙은 VFID를 힙 헤더 레코드에 보관하고,
B+Tree는 루트 헤더에 저장하면서 BTID_INT::ovfid를 인메모리 미러로 유지한다.
2.2 heap_ovf_find_vfid — 힙의 find-or-create
섹션 제목: “2.2 heap_ovf_find_vfid — 힙의 find-or-create”heap_ovf_find_vfid는 overflow 파일이 필요한 모든 힙 호출자의 단일 진입점이다.
docreate 인자가 동작 방식을 결정한다 — “없으면 생성”(삽입)과 “조회만”(수정/삭제).
헤더 latch 방향도 이에 따른다: 생성자는 WRITE, 조회자는 READ.
// heap_ovf_find_vfid -- src/storage/heap_file.c mode = (docreate == true ? PGBUF_LATCH_WRITE : PGBUF_LATCH_READ); addr_hdr.pgptr = pgbuf_fix (thread_p, &vpid, OLD_PAGE, mode, latch_cond); if (addr_hdr.pgptr == NULL) return NULL; /* <- branch A: header fix failed */ // ... condensed: pgbuf_check_page_ptype (PAGE_HEAP) ... if (spage_get_record (..., HEAP_HEADER_AND_CHAIN_SLOTID, &hdr_recdes, PEEK) != S_SUCCESS) { pgbuf_unfix_and_init (thread_p, addr_hdr.pgptr); return NULL; } /* <- branch B: header unreadable */ heap_hdr = (HEAP_HDR_STATS *) hdr_recdes.data;헤더 페이지는 잘 알려진 위치(hfid->vfid / hfid->hpgid)에 위치한다. 레코드는
PEEK 방식으로 읽히므로 heap_hdr는 라이브 포인터이고, 이후 변경은 RVHF_STATS가
보호하는 인플레이스 수정이 된다.
전체 분기 흐름:
flowchart TB
A["헤더 페이지 fix\nmode = docreate ? WRITE : READ"] --> B{pgptr == NULL?}
B -- 예 --> R1["return NULL"]
B -- 아니오 --> D{spage_get_record\npeek HEAP_HDR_STATS 성공?}
D -- 실패 --> R2["unfix; return NULL"]
D -- 성공 --> E{VFID_ISNULL\novf_vfid?}
E -- "아니오, 존재함" --> F["VFID_COPY\nheap_hdr->ovf_vfid에서 복사"]
E -- "예, 없음" --> G{docreate?}
G -- "false" --> H["ovf_vfid = NULL\n호출자에게 파일 없음 반환"]
G -- "true" --> J["log_sysop_start\nfile_create_with_npages MOH, 1"]
J --> K{create AND tde lookup\nAND tde apply 모두 성공?}
K -- "아니오, 어느 단계든" --> X["log_sysop_abort\novf_vfid = NULL; goto exit"]
K -- 예 --> P["log_append_undo RVHF_STATS\nVFID_COPY into heap_hdr\nlog_append_redo RVHF_STATS\npgbuf_set_dirty; log_sysop_commit"]
F --> Z["exit: unfix; return ovf_vfid"]
H --> Z
P --> Z
X --> Z
Figure 2-2 — heap_ovf_find_vfid의 모든 분기. K 노드는 세 개의 독립적 실패 지점
(create, TDE lookup, TDE apply)을 통합하며, 각각 sysop을 중단하고 NULL을 반환한다.
생성하지 않는 경우(VFID_ISNULL이 false이거나 docreate == false)가 정상 상태다:
파일이 한 번 생성되면 이후 호출은 모두 VFID_COPY 경로를 탄다. 생성 arm은
**최상위 시스템 오퍼레이션(top system operation)**으로 감싸진다:
// heap_ovf_find_vfid -- src/storage/heap_file.c (create arm, condensed) log_sysop_start (thread_p); HFID_COPY (&des.heap_overflow.hfid, hfid); /* <- descriptor: owning heap */ des.heap_overflow.class_oid = heap_hdr->class_oid; /* <- + class, for TDE */ if (file_create_with_npages (..., FILE_MULTIPAGE_OBJECT_HEAP, 1, &des, ovf_vfid) != NO_ERROR) { log_sysop_abort (thread_p); ovf_vfid = NULL; goto exit; } /* <- rolled back */ // ... TDE lookup + apply, each with its own abort+goto ... log_append_undo_data (thread_p, RVHF_STATS, &addr_hdr, sizeof (*heap_hdr), heap_hdr); VFID_COPY (&heap_hdr->ovf_vfid, ovf_vfid); /* <- the actual header mutation */ log_append_redo_data (thread_p, RVHF_STATS, &addr_hdr, sizeof (*heap_hdr), heap_hdr); pgbuf_set_dirty (thread_p, addr_hdr.pgptr, DONT_FREE); log_sysop_commit (thread_p);순서가 중요하다: VFID_COPY는 두 로그 호출 사이에 위치하므로, undo는 복사 전의
전체 HEAP_HDR_STATS를(null VFID), redo는 복사 후를(신규 VFID) 캡처한다 —
RVHF_STATS가 구조체 전체를 통째로 재생하기 때문이다. TDE는 헤더가 파일을 가리키기
전에, 동일한 sysop 안에서 적용된다.
불변 조건: 힙 헤더 VFID 쓰기와 파일 생성은 외부 트랜잭션 관점에서 원자적이다.
file_create_with_npages를 감싸는 log_sysop_start / log_sysop_commit 괄호와
RVHF_STATS undo/redo 쌍이 이를 강제한다. 이 불변 조건이 깨지면 — 파일은 생성됐으나
헤더 쓰기가 유실되면 — 힙은 고아 FILE_MULTIPAGE_OBJECT_HEAP을 갖게 되고,
다음 삽입 시 두 번째 파일이 생성된다.
2.3 힙 big record의 홈 참조 — REC_BIGONE
섹션 제목: “2.3 힙 big record의 홈 참조 — REC_BIGONE”heap_ovf_find_vfid는 파일만 반환한다. overflow_insert(3챕터)가 VPID로
생성하는 체인의 헤드 페이지는 heap_ovf_insert에 의해 홈 참조로 변환된다:
// heap_ovf_insert -- src/storage/heap_file.c if (heap_ovf_find_vfid (thread_p, hfid, &ovf_vfid, true, ...) == NULL || overflow_insert (thread_p, &ovf_vfid, &ovf_vpid, recdes, FILE_MULTIPAGE_OBJECT_HEAP) != NO_ERROR) return NULL; /* <- find/create or insert failed */ ovf_oid->pageid = ovf_vpid.pageid; ovf_oid->volid = ovf_vpid.volid; ovf_oid->slotid = NULL_SLOTID; /* Irrelevant */홈 슬롯 본체는 OID이지만, 의미 있는 필드는 volid와 pageid뿐이다 — 첫 번째
overflow 페이지를 가리키며, slotid는 NULL_SLOTID로 강제된다. overflow 레코드에는
슬롯 디렉토리가 없다(각 페이지의 data[]에서 바이트를 이어 붙이는 구조이므로),
/* Irrelevant */ 주석이 역참조를 시도하는 독자에게 경고를 준다.
불변 조건: REC_BIGONE 홈 OID는 항상 slotid == NULL_SLOTID를 갖는다. 이
단일 할당 지점에서 강제되며, big-record 경로가 헤드 페이지를 VPID로 fix할 때 전제된다.
null이 아닌 slotid는 포워딩 포인터가 오염된 징표다.
heap_ovf_insert는 ovf_oid만 채우며, REC_BIGONE 레코드 자체는 힙 삽입 레이어가
기록한다(read/update 챕터).
2.4 btree_create_overflow_key_file — B+Tree의 생성
섹션 제목: “2.4 btree_create_overflow_key_file — B+Tree의 생성”B+Tree에는 find 방식의 함수가 없다: 존재 여부는 VFID_ISNULL (&btid_int->ovfid)를
직접 확인하며, 생성은 BTID_INT::ovfid를 채우는 전용 함수가 담당한다:
// btree_create_overflow_key_file -- src/storage/btree.c VFID_SET_NULL (&btid->ovfid); des.btree_key_overflow.btid = *btid->sys_btid; /* structure copy */ des.btree_key_overflow.class_oid = btid->topclass_oid; // ... assert class_oid not null ... error_code = file_create_with_npages (thread_p, FILE_BTREE_OVERFLOW_KEY, 3, &des, &btid->ovfid); if (error_code != NO_ERROR) return error_code; /* <- branch A: create failed, ovfid stays NULL */ error_code = heap_get_class_tde_algorithm (thread_p, &btid->topclass_oid, &tde_algo); if (error_code != NO_ERROR) { VFID_SET_NULL (&btid->ovfid); return error_code; } /* <- branch B: roll back VFID */ error_code = file_apply_tde_algorithm (thread_p, &btid->ovfid, tde_algo); if (error_code != NO_ERROR) { VFID_SET_NULL (&btid->ovfid); return error_code; } /* <- branch C: roll back VFID */ return error_code; /* NO_ERROR: ovfid now valid */분기 커버리지와 힙 버전과의 차이점:
- 진입 시
VFID_SET_NULL— 어느 하위 단계에서 실패하더라도(branch A, B, C) 호출자에게 nullovfid가 남으며, 반쯤 설정된 VFID는 절대 남지 않는다. 이 함수는 인메모리BTID_INT::ovfid만 채우고log_sysop_start를 열지 않는다; 지속 가능한 루트 쓰기는 호출자의 책임이다. - 최소 3페이지 사전 할당 — 힙의 1페이지와 달리, 이후 overflow key 할당의 파일
확장 비용을 분산한다. TDE 상속은 힙과 동일한 방식으로 처리한다
(
heap_get_class_tde_algorithm).
호출자 — leaf-insert 경로 — 가 이를 내구성 있게, 안전하게 처리한다:
// btree leaf-insert path -- src/storage/btree.c (condensed; root is write latched) if (VFID_ISNULL (&btid_int->ovfid)) /* <- re-check under root WRITE latch */ { log_sysop_start (thread_p); error_code = btree_create_overflow_key_file (thread_p, btid_int); if (error_code != NO_ERROR) { log_sysop_abort (thread_p); goto error; } log_append_undoredo_data2 (thread_p, RVBT_UPDATE_OVFID, ..., *root_page, HEADER, sizeof (VFID), sizeof (VFID), &root_header->ovfid, &btid_int->ovfid); VFID_COPY (&root_header->ovfid, &btid_int->ovfid); /* <- persist into root header */ pgbuf_set_dirty (thread_p, *root_page, DONT_FREE); log_sysop_commit (thread_p); }힙 경로에는 없는 두 가지 포인트:
- null 체크가 루트 write latch 하에서 반복된다 — 소스 주석 “Check that overflow key file was not created by another.” 이 재확인 없이는 동시 삽입자 두 명이 각자 파일을 생성할 수 있다; latch 덕분에 늦은 쪽은 먼저 온 쪽의 VFID를 관찰하게 된다.
RVBT_UPDATE_OVFID는undoredo2방식으로 루트HEADER슬롯에 스코프되어 VFID 필드만 로그에 기록한다(old=undo, new=redo).RVHF_STATS처럼 헤더 전체를 기록하지 않는다. sysop은 호출자에 위치하므로, 생성 실패 시 호출 지점에서 abort된다.
불변 조건: B+Tree당 FILE_BTREE_OVERFLOW_KEY는 최대 하나이며, 생성 후에는
루트 헤더 ovfid와 BTID_INT::ovfid가 동일하다. latch 하의 재확인과
RVBT_UPDATE_OVFID undoredo가 루트 쓰기를 파일 생성 sysop에 묶어 이를 강제한다.
이 불변 조건이 깨지면 파일이 누출되고 overflow key 체인이 분기된다.
2.5 페이지 타입 규칙 — PAGE_OVERFLOW vs PAGE_BTREE
섹션 제목: “2.5 페이지 타입 규칙 — PAGE_OVERFLOW vs PAGE_BTREE”세 가지 체인 종류는 두 가지 페이지 타입으로 나뉜다; 디스크 레벨의 식별자는 파일이 아니라 페이지 타입이다:
| 체인 종류 | 소유 파일 | 페이지 타입 | 헤드 참조 필드 | 리프 플래그 |
|---|---|---|---|---|
| 힙 big record | FILE_MULTIPAGE_OBJECT_HEAP | PAGE_OVERFLOW | REC_BIGONE OID 본체 {volid, pageid} | n/a |
| B+Tree overflow key | FILE_BTREE_OVERFLOW_KEY (ovfid) | PAGE_OVERFLOW | leaf/non-leaf 레코드에 내장된 헤드 VPID | BTREE_LEAF_RECORD_OVERFLOW_KEY (0x4000) |
| B+Tree overflow OID list | btree_get_new_page를 통한 트리 파일의 페이지 | PAGE_BTREE | LEAF_REC.ovfl (a VPID) | BTREE_LEAF_RECORD_OVERFLOW_OIDS (0x2000) |
각주: LEAF_REC은 { VPID ovfl; short key_len; }이며, OID 목록 overflow가 없을 때
ovfl은 VPID_SET_NULL이다. overflow key 참조는 인라인 키 바이트 대신 레코드 본체를 재사용한다.
overflow key 경로는 overflow_insert를 통하며, PAGE_TYPE ptype = PAGE_OVERFLOW로
설정하고 이를 페이지당 초기화 함수로 file_alloc_multiple에 전달하며, 각 페이지에
pgbuf_check_page_ptype (..., PAGE_OVERFLOW)를 단언한다. overflow OID list 경로는
overflow_file.c를 전혀 건드리지 않는다: 해당 페이지는 btree_start_overflow_page →
btree_get_new_page에서 오고, 초기화 함수는 btree_initialize_new_page이다:
// btree_initialize_new_page -- src/storage/btree.c pgbuf_set_page_ptype (thread_p, page, PAGE_BTREE); /* <- not PAGE_OVERFLOW */ spage_initialize (thread_p, page, UNANCHORED_KEEP_SEQUENCE, BTREE_MAX_ALIGN, ...); /* slotted */결정적으로, btree_get_new_page는 btid->sys_btid->vfid — 메인 트리 파일 —
에서 할당한다. ovfid가 아니다. 따라서 OID list overflow 페이지는 “평범한 B+Tree
페이지”이다: 슬롯 방식, PAGE_BTREE(7챕터). overflow key 체인은 힙 big record와
바이트 레벨에서 동일하다: PAGE_OVERFLOW, 슬롯 디렉토리 없음, OVERFLOW_*_PART 헤더.
불변 조건: 페이지 타입이 두 가지 B+Tree overflow 용도를 구분한다. LEAF_REC.ovfl로
도달하는 페이지는 PAGE_BTREE여야 하고, overflow key VPID로 도달하는 페이지는
PAGE_OVERFLOW여야 한다. pgbuf_check_page_ptype 단언이 이를 강제하며, 혼동하면
OVERFLOW_FIRST_PART 스트림이 슬롯 페이지 접근에 공급된다.
세 가지 헤드 참조는 모두 체인 헤드를 향한 전방 포인터이며, 체인 자체가 아니다;
내부 링크(next_vpid / BTREE_OVERFLOW_HEADER.next_vpid)는 3, 4, 7챕터에서 다룬다.
2.6 챕터 요약 — 핵심 정리
섹션 제목: “2.6 챕터 요약 — 핵심 정리”- 지연 프로비저닝, 홈 오브젝트 헤더에 저장. 힙은 VFID를
HEAP_HDR_STATS.ovf_vfid에, B+Tree는 루트 헤더ovfid와BTID_INT::ovfid인메모리 미러에 보관한다. heap_ovf_find_vfid는 find-or-create를 하나의 함수에서 처리한다.docreate=true(오직heap_ovf_insert에서만)는 sysop 아래 1페이지FILE_MULTIPAGE_OBJECT_HEAP을 생성하고, 클래스 TDE를 적용하며,RVHF_STATSundo/redo를 기록한다.docreate=false는 기존 VFID를 읽는다.btree_create_overflow_key_file은BTID_INT::ovfid만 채우고, 호출자가 이를 영속화한다. 최소 3페이지를 사전 할당하고 클래스 TDE를 적용하며, 실패 시 VFID를 null로 돌린다. 호출자는 루트 write latch 하에서VFID_ISNULL을 재확인하고RVBT_UPDATE_OVFID로 루트 쓰기를 기록한다.- 힙 홈 참조는
slotid = NULL_SLOTID인REC_BIGONEOID이다.{volid, pageid}만 유효하다 — overflow 레코드에는 슬롯 디렉토리가 없다. - 디스크 레벨 식별자는 파일이 아니라 페이지 타입이다. 힙 big-record와
B+Tree overflow key 체인은
PAGE_OVERFLOW를 사용하고, overflow OID list 체인은btree_get_new_page/btree_initialize_new_page로 할당되며PAGE_BTREE를 사용한다. - 각 체인 종류는 고유한 소유 파일을 갖는다. B+Tree overflow key 체인은
BTID_INT::ovfid(FILE_BTREE_OVERFLOW_KEY)에, overflow OID list 체인은 메인 트리 파일 (btid->sys_btid->vfid)에 위치한다. 리프 플래그BTREE_LEAF_RECORD_OVERFLOW_KEY(0x4000) vsBTREE_LEAF_RECORD_OVERFLOW_OIDS(0x2000)로 어느 쪽을 따를지 판단한다. - 모든 VFID 쓰기는 크래시 앞에서 원자적이다. 두 서브시스템 모두 파일 생성과 헤더/VFID 쓰기를 sysop으로 감싸므로, 크래시 후 파일도 참조도 없거나 둘 다 있거나 — 파일만 누출된 중간 상태는 없다.
Chapter 3: overflow_insert로 체인 만들기
섹션 제목: “Chapter 3: overflow_insert로 체인 만들기”이 챕터는 하나의 질문에 답한다. 여러 페이지에 걸친 RECDES와 VFID가 주어졌을 때, CUBRID는 어떻게 전체 overflow 체인 — 페이지 할당, 페이로드 분배, 선행 로깅(WAL) — 을 단일 순방향 패스에서 원자적으로 구성하는가? 그 답이 overflow_insert다. 이를 호출하는 두 caller, heap_ovf_insert와 btree_store_overflow_key는 RECDES를 조립하고 FILE_TYPE을 선택한 뒤, 첫 페이지의 VPID를 caller 친화적인 주소로 변환하는 역할만 한다. 이 세 함수와 모든 분기를 낱낱이 해부한다.
체인 레이아웃(OVERFLOW_FIRST_PART / OVERFLOW_REST_PART)과 “overflow가 왜 존재하는가”에 관한 이론은 Chapter 1과 동반 문서 cubrid-overflow-file.md를 참고하라. 여기서는 쓰기 경로에 집중한다.
3.1 writer가 채우는 두 구조체 (요약 복습)
섹션 제목: “3.1 writer가 채우는 두 구조체 (요약 복습)”overflow_insert는 페이지에 두 가지 헤더 형태를 기록한다 — OVERFLOW_FIRST_PART(페이지 0)와 OVERFLOW_REST_PART(페이지 1..n-1). 각 필드의 상세 설명은 Chapter 1에서 다뤘으며, 쓰기 경로에서 실제로 건드리는 필드는 세 가지다. next_vpid는 두 구조체 모두에 있으며, vpids[i+1]에서 가져온 전방 링크로서 마지막 페이지와 npages == 1인 경우 페이지 0에서는 NULL이다. length는 first 구조체에만 있으며, 전체 레코드의 바이트 수를 담아 reader가 체인을 끝까지 탐색하지 않고도 버퍼 크기를 결정할 수 있게 한다(Chapter 4). data[1]은 두 구조체 모두에 있는 유연 배열로, 헤더 뒤에 memcpy되며 offsetof(..., data)가 페이지당 페이로드 용량이 된다.
OVERFLOW_REST_PART는 length를 생략하기 때문에, rest 페이지의 페이로드 용량은 first 페이지보다 정확히 sizeof(int) 만큼 크다 — 이 비대칭이 3.2절의 수식이 성립하는 근거다.
3.2 선행 정확 페이지 수 계산
섹션 제목: “3.2 선행 정확 페이지 수 계산”디스크에 손을 대기 전, overflow_insert는 정확한 페이지 수를 계산한다 — 나중에 보정하는 추정치가 아니라 정확한 값이며, 뒤따르는 assert(length == 0)(3.7)이 그 정확성을 증명한다.
// overflow_insert -- src/storage/overflow_file.clength = recdes->length - (DB_PAGESIZE - (int) offsetof (OVERFLOW_FIRST_PART, data));if (length > 0) { i = DB_PAGESIZE - offsetof (OVERFLOW_REST_PART, data); /* rest-page payload capacity */ npages = 1 + CEIL_PTVDIV (length, i); /* 1 first page + ceil(rest/cap) */ }else { npages = 1; /* whole record fits in page 0 */ }length는 “페이지 0에 들어가지 않는 바이트 수”라는 의미로 재활용된다. <= 0이면 npages == 1 경로가 실행된다. 그렇지 않으면 CEIL_PTVDIV가 나머지를 rest 페이지 용량으로 올림 나눗셈하고, 1 +로 페이지 0을 다시 더한다.
불변 조건 — 페이지 수는 정확한 값이며 상한값이 아니다. overflow_insert는 정확히 npages개를 할당하며, 루프는 레코드를 바이트 단위까지 소비해야 한다(assert(length == 0), 3.7). 두 offsetof 값이 어긋나면 루프는 바이트를 기록하지 못하거나 할당되지 않은 vpids[npages]를 인덱싱하게 된다.
3.3 VPID 배열: 스택 버퍼 대 malloc, 그리고 OOM 분기
섹션 제목: “3.3 VPID 배열: 스택 버퍼 대 malloc, 그리고 OOM 분기”overflow_insert는 npages + 1개의 VPID가 필요하다 — 페이지마다 하나, 그리고 sentinel 슬롯 하나. 일반적인 경우 힙 할당을 피하기 위해 스택 버퍼를 유지하고, 체인이 클 때만 malloc한다.
// overflow_insert -- src/storage/overflow_file.c#define OVERFLOW_ALLOCVPID_ARRAY_SIZE 64 /* (top of file) */VPID vpids_buffer[OVERFLOW_ALLOCVPID_ARRAY_SIZE + 1]; /* +1 for the sentinel slot */// ...if (npages > OVERFLOW_ALLOCVPID_ARRAY_SIZE) { vpids = (VPID *) malloc ((npages + 1) * sizeof (VPID)); if (vpids == NULL) { er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_OUT_OF_VIRTUAL_MEMORY, 1, (npages + 1) * sizeof (VPID)); return ER_OUT_OF_VIRTUAL_MEMORY; /* <- direct return: nothing acquired yet */ } }else { vpids = vpids_buffer; /* point at the stack buffer */ }npages > 64malloc OOM →er_set후 단순return,goto exit_on_error가 아니다: 아직 sysop이 시작되지 않았고 fix된 페이지도 없으므로 abort하거나 해제할 것이 없다.npages <= 64→vpids는vpids_buffer의 별칭이 된다.if (vpids != vpids_buffer)가 유일한free신호다. 임계값은 공간/속도 트레이드오프 — 대부분의 레코드는 64 페이지 미만이다.
debug 전용(!NDEBUG) 사전 초기화 및 할당 후 assert. NDEBUG가 꺼진 빌드에서는 for (i...) VPID_SET_NULL(&vpids[i])로 모든 슬롯을 null로 채운 뒤, file_alloc_multiple 이후 대칭 루프에서 !VPID_ISNULL(&vpids[i])를 assert한다. 이 null 초기화는 debug 전용이지만, sentinel VPID_SET_NULL(&vpids[npages])는 모든 빌드에서 실행된다. 루프는 마지막 페이지의 next_vpid에 이를 기록하는데(i == npages-1일 때 vpids[i+1]이 vpids[npages]를 읽는다), 이로써 체인이 종료된다. 이 sentinel이 없으면 마지막 전방 링크가 garbage가 되어 Chapter 4의 순회가 끝나지 않는다.
3.4 시스템 오퍼레이션 열기와 전체 페이지 일괄 할당
섹션 제목: “3.4 시스템 오퍼레이션 열기와 전체 페이지 일괄 할당”// overflow_insert -- src/storage/overflow_file.clog_sysop_start (thread_p);is_sysop_started = true;error_code = file_alloc_multiple (thread_p, ovf_vfid, file_type != FILE_TEMP ? file_init_page_type : file_init_temp_page_type, &ptype, npages, vpids);if (error_code != NO_ERROR) { ASSERT_ERROR (); goto exit_on_error; }*ovf_vpid = vpids[0]; /* head VPID returned to caller */log_sysop_start는 중첩 시스템 오퍼레이션을 연다. 성공 시 외부 트랜잭션에 연결되고, 실패 시 abort된다(둘 다 3.7에서 처리). 이로써 체인 생성은 원자적이 된다. file_alloc_multiple은 npages개를 한 번에 확보하며(vpids[0..npages-1] 채움), initializer는 file_type에 따라 분기한다. FILE_TEMP이면 file_init_temp_page_type, 그 외에는 file_init_page_type — 둘 다 &ptype으로 PAGE_OVERFLOW를 스탬핑한다. 실패 시 goto exit_on_error. 유일한 출력 *ovf_vpid = vpids[0]이 체인 head다.
3.5 페이지별 복사 루프 — first-part 대 rest-part 분기
섹션 제목: “3.5 페이지별 복사 루프 — first-part 대 rest-part 분기”단일 for 루프가 각 페이지를 fix하고, 헤더와 페이로드 슬라이스를 기록하고, 로깅하고, 해제한다. 구조적 분기는 i == 0(first part)과 else(rest) 두 가지뿐이다.
// overflow_insert -- src/storage/overflow_file.cdata = recdes->data;length = recdes->length; /* re-used: now the running remainder */for (i = 0; i < npages; i++) { addr.pgptr = pgbuf_fix (thread_p, &vpids[i], OLD_PAGE, PGBUF_LATCH_WRITE, PGBUF_UNCONDITIONAL_LATCH); if (addr.pgptr == NULL) { ASSERT_ERROR_AND_SET (error_code); goto exit_on_error; } /* fix failed: abort whole chain */ // ... !NDEBUG pgbuf_check_page_ptype (addr.pgptr, PAGE_OVERFLOW) ... if (i == 0) { first_part = (OVERFLOW_FIRST_PART *) addr.pgptr; first_part->next_vpid = vpids[i + 1]; /* link to page 1 (or sentinel NULL) */ first_part->length = length; /* TOTAL length, written once */ copyto = (char *) first_part->data; copy_length = DB_PAGESIZE - offsetof (OVERFLOW_FIRST_PART, data); if (length < copy_length) copy_length = length; /* last/only page: clamp */ if (file_type != FILE_TEMP) log_append_empty_record (thread_p, LOG_DUMMY_OVF_RECORD, &addr); /* first page only */ } else { /* rest part: identical, minus the length field */ rest_parts = (OVERFLOW_REST_PART *) addr.pgptr; rest_parts->next_vpid = vpids[i + 1]; /* sentinel NULL on last page */ copyto = (char *) rest_parts->data; copy_length = DB_PAGESIZE - offsetof (OVERFLOW_REST_PART, data); if (length < copy_length) copy_length = length; } memcpy (copyto, data, copy_length); // ... redo logging (3.6) ... data += copy_length; /* advance source cursor */ length -= copy_length; /* shrink remainder */ pgbuf_set_dirty_and_free (thread_p, addr.pgptr); }각 페이지는 한 이터레이션 안에서 fix-then-free되므로 동시에 최대 한 페이지만 latch를 보유한다. pgbuf_fix 실패 시 3.7의 abort가 나머지를 정리한다. length < copy_length 클램프는 마지막 부분 페이지에서 발동하며, npages == 1인 경우(단독 페이지)에도 동일하게 적용된다.
flowchart TD
FIX["pgbuf_fix vpids[i] WRITE latch"]
FIX -->|NULL| ERR["ASSERT_ERROR_AND_SET; goto exit_on_error"]
FIX -->|ok| Q{"i == 0 ?"}
Q -->|yes| FIRST["first_part: next_vpid, length,\nfirst-part 용량, clamp;\nFILE_TEMP 아니면 LOG_DUMMY_OVF_RECORD"]
Q -->|no| REST["rest_parts: next_vpid,\nrest-part 용량, clamp"]
FIRST --> CP["memcpy copy_length"]
REST --> CP
CP --> REDO{"FILE_TEMP 아니고\nno_logging != true ?"}
REDO -->|yes| LOGR["log_append_redo_data RVOVF_NEWPAGE_INSERT"]
REDO -->|no| ADV["data += copy_length; length -= copy_length"]
LOGR --> ADV
ADV --> FREE["pgbuf_set_dirty_and_free"]
FREE --> NEXT{"i+1 < npages ?"}
NEXT -->|yes| FIX
NEXT -->|no| DONE["assert length == 0"]
Figure 3-1. 복사 루프 한 패스: fix 실패, first/rest 헤더 분기, redo 로깅 분기.
3.6 로깅: 더미 첫 페이지 마커와 페이지별 redo
섹션 제목: “3.6 로깅: 더미 첫 페이지 마커와 페이지별 redo”두 가지 로그 레코드가 발행된다(3.5 코드 발췌에 모두 보인다).
LOG_DUMMY_OVF_RECORD(log_append_empty_record,i == 0분기) — 빈 레코드로,FILE_TEMP가 아닌 체인의 페이지 0에만, 한 번만 기록된다. 로그 상에서 체인 헤드를 고정하여 recovery/vacuum이 이를 소유 레코드와 연관지을 수 있게 한다(Chapter 8). temp 파일은 복구 대상이 아니므로 생략한다.RVOVF_NEWPAGE_INSERT(log_append_redo_data,memcpy이후) — 페이지마다 기록되며,file_type != FILE_TEMP그리고thread_p->no_logging != true두 조건을 모두 만족해야 발행된다. 로깅 길이copy_length + CAST_BUFLEN(copyto - addr.pgptr)는 기록된 접두 전체(헤더 및 데이터)를 포함하므로 redo 한 번으로 페이지를 재구성할 수 있다.no_logging은 실제 페이지를 할당하면서도 redo를 억제하는 벌크 경로를 위한 것이다.
불변 조건 — temp 체인은 절대 로깅하지 않고, persistent 체인은 no_logging이 아닌 한 항상 redo를 로깅한다. 이 게이팅은 3.4의 페이지 초기화 분기와 반드시 일치해야 한다. temp 파일에 더미 레코드를 남기면 recovery가 크래시 후 살아남지 못한 체인을 따라가고, persistent 체인에서 redo를 생략하면 페이지가 stale 상태로 남는다.
3.7 성공 및 실패 에필로그
섹션 제목: “3.7 성공 및 실패 에필로그”두 종료 경로는 sysop 브래킷을 중심으로 서로를 대칭적으로 반영한다.
// overflow_insert -- src/storage/overflow_file.cassert (length == 0); /* 3.2's "exact" invariant, proven here */#if defined (CUBRID_DEBUG) if (length > 0) { assert (false); er_log_debug (ARG_FILE_LINE, "ovf_insert: ** SYSTEM ERROR ... # of pages ... incorrect"); error_code = ER_FAILED; goto exit_on_error; } /* recoverable miscalc, not a hard assert */#endif log_sysop_attach_to_outer (thread_p); /* SUCCESS: undone with the outer op, not standalone */ if (vpids != vpids_buffer) free_and_init (vpids); return NO_ERROR;
exit_on_error: if (is_sysop_started) log_sysop_abort (thread_p); /* roll back every page alloc + write */ if (vpids != vpids_buffer) free_and_init (vpids); return error_code;성공 시, length가 0이 아니라면 3.2의 수식과 루프가 불일치한 것이다. CUBRID_DEBUG에서만 이를 goto exit_on_error를 통한 복구 가능한 ER_FAILED 롤백으로 에스컬레이션하며, 일반 빌드에서는 bare assert에 그친다. log_sysop_attach_to_outer는 sysop을 독립 커밋(sysop_commit)하는 대신 외부 트랜잭션의 undo 체인에 연결하므로, 체인은 외부 오퍼레이션과 함께 undo된다. 에러 경로에서는 is_sysop_started 플래그가 abort를 보호한다 — log_sysop_start 이전에 label에 도달하는 goto는 없다(초기 malloc-OOM은 label을 피해 return한다). 따라서 label에 도착하면 sysop을 abort하여 file_alloc_multiple을 되돌리고 redo를 파기한다. 두 종료 경로는 모두 vpids != vpids_buffer free 가드를 공유한다.
불변 조건 — 체인 생성은 전부 아니면 전무다. overflow_insert는 NO_ERROR를 반환하며 완전히 기록되고, 로깅되고, 외부에 연결된 체인을 남기거나, 에러를 반환하며 한 페이지도 남기지 않는다 — sysop 브래킷이 이를 강제한다. 루프 중간에 커밋하면 크래시 시 할당됐지만 비어있는, 소유자 없는 페이지가 남게 된다.
3.8 overflow_insert를 호출하는 두 래퍼
섹션 제목: “3.8 overflow_insert를 호출하는 두 래퍼”두 개의 얇은 래퍼가 overflow_insert의 인수를 준비한다. **heap_ovf_insert(힙 멀티페이지 오브젝트)**는 힙의 overflow VFID를 확인하고, FILE_MULTIPAGE_OBJECT_HEAP으로 overflow_insert를 호출한 뒤, head VPID를 OID로 변환한다.
// 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; /* no overflow file, or insert failed */ovf_oid->pageid = ovf_vpid.pageid; ovf_oid->volid = ovf_vpid.volid;ovf_oid->slotid = NULL_SLOTID; /* overflow OIDs have no slot */return ovf_oid;||는 두 실패 분기를 하나로 묶는다(true 인수는 overflow 파일이 없을 경우 생성한다). 성공 시 head VPID가 ovf_oid의 pageid/volid를 채우며, slotid = NULL_SLOTID — 슬롯이 아닌 페이지 단위로 주소가 지정된다(Chapter 2).
btree_store_overflow_key(초과 크기 인덱스 키). DB_VALUE 키를 RECDES로 직렬화한 뒤 FILE_BTREE_OVERFLOW_KEY로 overflow_insert를 호출한다.
// btree_store_overflow_key -- src/storage/btree.cif (src_type != dst_type) /* key domain differs from index domain */ { key_ptr = &new_key; status = tp_value_cast (key, key_ptr, tp_domain, false); if (status != DOMAIN_COMPATIBLE) { assert (false); goto exit_on_error; } size = btree_get_disk_size_of_key (key_ptr); }rec.data = (char *) db_private_alloc (thread_p, size); /* OOM -> er_set + goto exit_on_error */// ... or_init + pr_type->index_writeval (failure -> goto exit_on_error); rec.length = serialized len ...if (overflow_insert (thread_p, &overflow_file_vfid, first_overflow_page_vpid, &rec, FILE_BTREE_OVERFLOW_KEY) != NO_ERROR) { ASSERT_ERROR (); goto exit_on_error; }노드 타입이 tp_domain을 사전 선택하며, 도메인 불일치 시 키를 캐스팅한다(non-DOMAIN_COMPATIBLE은 assert(false) 처리). db_private_alloc OOM, index_writeval 실패, overflow_insert 실패는 각각 goto exit_on_error로 이어진다(rec.data/key_ptr 해제, ER_FAILED로 정규화). 힙과 달리 btree는 *raw VPID*를 유지한다 — 인덱스가 이미 체인을 소유하기 때문이다.
3.9 챕터 요약 — 핵심 정리
섹션 제목: “3.9 챕터 요약 — 핵심 정리”- 페이지 수는 선행에서 정확하게 계산된다:
npages = 1 + CEIL_PTVDIV(rest_bytes, rest_capacity)또는1. 첫 페이지는sizeof(int)만큼 페이로드가 적다(length필드를 담기 때문) — 이 부담이 되는 비대칭은assert(length == 0)으로 정확성이 증명된다. - 64슬롯 스택 버퍼로 대부분의 경우 malloc을 피한다.
npages > OVERFLOW_ALLOCVPID_ARRAY_SIZE일 때만 malloc하며(OOM 시 직접 return),vpids != vpids_buffer가 유일한free신호다.vpids[npages]sentinel은 모든 빌드에서 마지막next_vpid를 종료한다. - 체인은 단일 시스템 오퍼레이션 안에서 구성된다 —
log_sysop_start…file_alloc_multiple… 루프 … 성공 시log_sysop_attach_to_outer/ 에러 시log_sysop_abort: 전부 아니면 전무. - 루프의 유일한 구조적 분기는 first-part 대 rest-part다. 페이지 0은
next_vpid+ 전체length+ 페이로드를 기록하고, rest 페이지는length를 생략한다. 각 이터레이션은 한 페이지를 fix, 기록, 로깅, 해제한다 — 단일 페이지 latch 풋프린트. - 로깅은 파일 타입으로 게이팅되며, 래퍼는 인수만 조립한다.
LOG_DUMMY_OVF_RECORD는 페이지 0에 한 번,RVOVF_NEWPAGE_INSERT는 페이지마다 — 둘 다 비temp 파일에서만(redo는no_logging으로도 게이팅).heap_ovf_insert는FILE_MULTIPAGE_OBJECT_HEAP을 선택하고NULL_SLOTID를 가진OID를 반환한다.btree_store_overflow_key는FILE_BTREE_OVERFLOW_KEY를 선택하고 rawVPID를 유지한다.
Chapter 4: 체인 읽기와 순회
섹션 제목: “Chapter 4: 체인 읽기와 순회”3장에서 체인을 구성했다. 총 length를 보유하는 OVERFLOW_FIRST_PART 헤드 페이지 하나, 그리고 next_vpid로 연결된 0개 이상의 OVERFLOW_REST_PART 페이지가 NULL까지 이어지는 구조다. 이 장에서 다루는 질문은 헤드 VPID가 주어졌을 때, 호출자가 체인의 전부 또는 일부를 어떻게 읽으며, 그 순회를 어떻게 안전하게 유지하는가이다. 읽기 인터페이스는 작다 — 공개 진입점 세 개, 진단용 순회 하나, 플러시 순회 하나, 내부 함수 두 개 — 이 중 어느 것도 lock을 잡지 않는다. 홈 레퍼런스의 row/key lock이 접근 관문 역할을 한다(companion §“Pages not individually locked”).
4.1 리더가 순회하는 두 구조체
섹션 제목: “4.1 리더가 순회하는 두 구조체”새로운 구조체는 도입되지 않는다. 읽기는 1장에서 정의한 것을 소비한다(overflow_first_part = {VPID next_vpid; int length; char data[1];}, overflow_rest_part = {VPID next_vpid; char data[1];}). 각 필드의 상세 설계 근거는 1장을 참고하고, 여기서는 읽기 시점의 역할만 정리한다.
| 필드 | 역할 | 이유 |
|---|---|---|
overflow_first_part.next_vpid | 헤드의 data[]를 소비한 뒤 따라가는 2번째 페이지 링크 | 헤드의 offset-0에서만 읽는다(VPID_EQ로 판단); 여기가 NULL이면 단일 페이지 레코드 |
overflow_first_part.length | 전체 바이트 수를 O(1)로 미리 파악하는 권위 있는 값 | 헤드 전용이므로 길이 조회 시 순회 불필요; 바이트 예산(4.4)과 페이지 수 계산(4.9)의 기준값 |
overflow_first_part.data[] | 첫 번째 페이로드 슬라이스 | 앞쪽 OR_MVCC_MAX_HEADER_SIZE 바이트가 스냅샷 게이트가 읽는 MVCC 헤더 |
overflow_rest_part.next_vpid | 다음 rest 페이지 링크 | 동일한 offset-0 위치; NULL이 체인 종료를 표시 — 바이트가 남아 있는데 NULL이면 데이터 손상(4.4) |
overflow_rest_part.data[] | 페이로드 연속 데이터 | rest 페이지는 length가 없으므로 헤드보다 sizeof(int) 만큼 더 많은 페이로드를 담는다 |
불변 조건 (ID 기반 헤드/rest 구별). 읽기 중 어떤 페이지가 헤드인지는 그 VPID가 호출자의
ovf_vpid와 같은지로만 결정된다 — “is-head” 플래그는 없고VPID_EQ비교(overflow_next_vpid, 4.2)만 있다. rest 페이지 VPID를 넘기면,length4바이트를data[]시작 부분으로 잘못 읽어 조용히 오순회한다. 따라서 모든 진입점은 홈REC_BIGONE/ 리프 레퍼런스에서 가져온 첫 번째 페이지 VPID를 받아야 한다.
flowchart LR HEAD["헤드 페이지 == ovf_vpid\nnext_vpid -> R1\nlength = 전체 길이\ndata[ MVCC hdr then body ]"] R1["rest 페이지\nnext_vpid -> R2\ndata[ body ]"] R2["rest 페이지\nnext_vpid = NULL\ndata[ body end ]"] HEAD -->|next_vpid| R1 -->|next_vpid| R2
Figure 4-1 — 리더가 보는 구조: 헤드는 오직 호출자의 VPID와의 동등성으로만 구별되며, length는 헤드에만 존재한다.
4.2 overflow_next_vpid — 헤드/rest 선택자
섹션 제목: “4.2 overflow_next_vpid — 헤드/rest 선택자”// overflow_next_vpid -- src/storage/overflow_file.cif (VPID_EQ (ovf_vpid, vpid)) /* <- still the original head? */ *vpid = ((OVERFLOW_FIRST_PART *) pgptr)->next_vpid; /* link sits after length */else *vpid = ((OVERFLOW_REST_PART *) pgptr)->next_vpid; /* rest layout, offset 0 */분기는 두 개, 오류 경로는 없다. overflow_traverse만이 이 함수를 호출한다(매 반복마다 ovf_vpid에서 다시 순회하며 “아직 헤드인가?”를 물어야 한다). overflow_get_nbytes와 overflow_get_capacity는 판별 로직을 인라인으로 구현한다 — 루프 전에 헤드를 처리하고, 루프 본체는 rest 페이지만 다룬다.
4.3 overflow_get_length — O(1) 헤드 단독 읽기
섹션 제목: “4.3 overflow_get_length — O(1) 헤드 단독 읽기”// overflow_get_length -- src/storage/overflow_file.cpgptr = pgbuf_fix (thread_p, ovf_vpid, OLD_PAGE, PGBUF_LATCH_READ, PGBUF_UNCONDITIONAL_LATCH);if (pgptr == NULL) return -1; /* <- only failure branch */length = ((OVERFLOW_FIRST_PART *) pgptr)->length; /* head-only field */pgbuf_unfix_and_init (thread_p, pgptr);return length;오류 분기 하나(-1 반환), 성공 경로 하나, 읽기 전용이므로 PGBUF_LATCH_READ. -1 센티넬 값은 btree_load_overflow_key(4.7)가 버퍼 크기를 결정하기 전에 확인하는 값이다.
4.4 overflow_get_nbytes — 부분 읽기, 분기별 분석
섹션 제목: “4.4 overflow_get_nbytes — 부분 읽기, 분기별 분석”이 장의 핵심 함수다. start_offset부터 max_nbytes 바이트를 읽고, MVCC 게이트를 적용하고, 버퍼 크기 부족 힌트를 전달하며, 페이지 경계를 넘어 복사한다. overflow_get(4.5)과 heap_ovf_get(4.6)이 이 함수를 래핑한다.
Step 1 — 헤드 fix: pgbuf_fix로 헤드를 고정한다. fix 실패 시 S_ERROR 반환.
Step 2 — MVCC 게이트 (heap 경로 전용): 스냅샷이 있을 때만 실행된다. heap_get_mvcc_rec_header_from_overflow가 overflow_get_first_page_data (pgptr)(실제 접근자, ((OVERFLOW_FIRST_PART *) pgptr)->data 반환)을 RECDES에 연결한 뒤 or_mvcc_get_header로 헤드의 data[]를 디코딩한다.
// overflow_get_nbytes -- src/storage/overflow_file.cif (mvcc_snapshot != NULL) { heap_get_mvcc_rec_header_from_overflow (pgptr, &mvcc_header, NULL); /* decode head data[] */ if (mvcc_snapshot->snapshot_fnc (thread_p, &mvcc_header, mvcc_snapshot) == TOO_OLD_FOR_SNAPSHOT) { /* not satisfied ONLY on TOO_OLD; TOO_NEW (e.g. just-updated record locked at select) is accepted */ pgbuf_unfix_and_init (thread_p, pgptr); return S_SNAPSHOT_NOT_SATISFIED; } }세 가지 판정 중 TOO_OLD_FOR_SNAPSHOT만 중단시킨다. TOO_NEW와 만족된 경우는 그냥 통과하므로, 방금 갱신된 big 레코드에 대한 SELECT ... FOR UPDATE도 lock을 걸기 위한 후보로 계속 처리된다. mvcc_snapshot == NULL이면(overflow-key 경로, 4.7) 이 블록 전체를 건너뛴다.
Step 3–4 — 바이트 예산 계산, 버퍼 크기 판정.
// overflow_get_nbytes -- src/storage/overflow_file.c*remaining_length = first_part->length;if (max_nbytes < 0) max_nbytes = *remaining_length - start_offset; /* "rest from offset" */else if (max_nbytes > *remaining_length - start_offset) max_nbytes = *remaining_length - start_offset; /* clamp */if (max_nbytes < 0) { max_nbytes = 0; *remaining_length = 0; } /* offset past record end -> empty */else *remaining_length -= max_nbytes;// ... condensed ...if (max_nbytes > recdes->area_size) /* too small */ { pgbuf_unfix_and_init (...); recdes->length = -max_nbytes; return S_DOESNT_FIT; } /* neg = needed bytes */else if (max_nbytes == 0) /* empty / past-end */ { pgbuf_unfix_and_init (...); recdes->length = 0; return S_SUCCESS; }recdes->length = max_nbytes;네 가지 예산/판정 분기 모두 인라인으로 주석 처리되어 있다. 핵심 세부 사항은 S_DOESNT_FIT 반환 시 필요한 크기를 음수 recdes->length로 전달한다는 점이다(호출자가 이 값을 보고 재시도한다 — 4.6).
Step 5 — 탐색 후 복사 루프, 헤드의 data[]에서 시작한다.
// overflow_get_nbytes -- src/storage/overflow_file.c (loop, condensed)data = recdes->data; copyfrom = (char *) first_part->data; next_vpid = first_part->next_vpid;while (max_nbytes > 0) { if (start_offset > 0) /* seek: copy_length = min(start_offset, bytes-left-on-page); advance copyfrom */ { ... ; start_offset -= copy_length; copyfrom += copy_length; } if (start_offset == 0) /* copy: copy_length = min(max_nbytes, bytes-left-on-page), page-bounded */ { ... ; if (copy_length > 0) { memcpy (data, copyfrom, copy_length); data += copy_length; max_nbytes -= copy_length; } } pgbuf_unfix_and_init (thread_p, pgptr); /* <- unfix BEFORE fixing the next page */ if (max_nbytes > 0) { if (VPID_ISNULL (&next_vpid)) /* corruption guard, see callout */ { er_set (..., ER_HEAP_OVFADDRESS_CORRUPTED, 3, ovf_vpid->volid, ovf_vpid->pageid, NULL_SLOTID); return S_ERROR; } pgptr = pgbuf_fix (thread_p, &next_vpid, OLD_PAGE, PGBUF_LATCH_READ, PGBUF_UNCONDITIONAL_LATCH); if (pgptr == NULL) { recdes->length = 0; return S_ERROR; } rest_parts = (OVERFLOW_REST_PART *) pgptr; /* every later page is a rest page */ copyfrom = (char *) rest_parts->data; next_vpid = rest_parts->next_vpid; } }return S_SUCCESS;각 ...은 현재 고정된 페이지에 남은 바이트만큼 copy_length를 제한한다. 탐색과 복사는 한 반복 안에서 이어진다(페이지 끝까지 탐색 후 즉시 복사로 fall-through).
불변 조건 (한 번에 하나의 페이지만 latch; 체인 길이 ≥
length바이트). 각 페이지는 다음 페이지를 fix하기 전에pgbuf_unfix_and_init된다 — 이로써 read-latch 점유 범위를 최소화하고 latch 순서 데드락을 방지한다. unfix-N/fix-(N+1) 구간 동안의 구조적 변경은 홈 레퍼런스의 lock이 직렬화한다(companion Open Question 5). 그리고max_nbytes > 0인 상태에서next_vpid가 NULL이면 체인이 선언된length보다 일찍 끝난 것이므로, 코드는ER_HEAP_OVFADDRESS_CORRUPTED를 발생시킨다 — 쓰기 측이length와 페이지 수를 일치시켜야 한다는 의무의 대응 쌍이다.
4.5 overflow_get — (0, -1) 래퍼
섹션 제목: “4.5 overflow_get — (0, -1) 래퍼”한 줄짜리 래퍼다. overflow_get은 overflow_get_nbytes (..., 0, -1, &remaining_length, mvcc_snapshot)을 반환한다 — start_offset = 0, max_nbytes = -1(“전부”), remaining_length는 버림. heap_ovf_get(4.6)과 btree_load_overflow_key(4.7)가 전체 레코드를 읽을 때 이 함수를 호출한다.
4.6 heap 읽기 호출자들
섹션 제목: “4.6 heap 읽기 호출자들”heap_ovf_get은 REC_BIGONE OID를 VPID로 변환하고, CHN(cache-coherency-number) 빠른 경로를 추가한다.
// heap_ovf_get -- src/storage/heap_file.c (condensed)ovf_vpid.pageid = ovf_oid->pageid; ovf_vpid.volid = ovf_oid->volid;if (chn != NULL_CHN) /* peek only the MVCC-header prefix to compare CHN */ { scan = overflow_get_nbytes (thread_p, &ovf_vpid, recdes, 0, OR_MVCC_MAX_HEADER_SIZE, &rest_length, mvcc_snapshot); if (scan == S_SUCCESS && chn == or_chn (recdes)) return S_SUCCESS_CHN_UPTODATE; /* cached copy current; skip full read */ }return overflow_get (thread_p, &ovf_vpid, recdes, mvcc_snapshot); /* full read */S_SUCCESS + CHN 일치 시에만 S_SUCCESS_CHN_UPTODATE로 단락된다. 그 외(짧은 읽기 S_SNAPSHOT_NOT_SATISFIED / S_DOESNT_FIT, 또는 NULL_CHN)는 전체 overflow_get으로 fall-through한다. heap_get_bigone_content가 S_DOESNT_FIT 재시도 루프를 소유한다.
// heap_get_bigone_content -- src/storage/heap_file.c (condensed)if (scan_cache != NULL && (ispeeking == PEEK || recdes->data == NULL || ...)) /* reusable area available */ { scan_cache->assign_recdes_to_area (*recdes); while ((scan = heap_ovf_get (thread_p, forward_oid, recdes, NULL_CHN, NULL)) == S_DOESNT_FIT) scan_cache->assign_recdes_to_area (*recdes, (size_t) (-recdes->length)); /* grow to needed size, retry */ if (scan != S_SUCCESS) recdes->data = NULL; }else scan = heap_ovf_get (thread_p, forward_oid, recdes, NULL_CHN, NULL); /* caller-owned buffer, no retry */return scan;두 분기 모두 인라인 주석으로 설명된다. 재사용 가능 영역 분기는 S_DOESNT_FIT에서 루프하고, 호출자 소유 버퍼 분기는 재시도하지 않는다. 양쪽 모두 NULL_CHN과 mvcc_snapshot = NULL을 전달한다 — 스냅샷은 REC_BIGONE에 도달한 가시 버전 순회 상류에서 이미 확인되었다(companion §“Walk: heap read of a big record”).
4.7 btree_load_overflow_key — 크기 확인, 할당, 읽기, 항상 복사
섹션 제목: “4.7 btree_load_overflow_key — 크기 확인, 할당, 읽기, 항상 복사”유일한 non-heap 소비자다.
// btree_load_overflow_key -- src/storage/btree.c (condensed)rec.area_size = overflow_get_length (thread_p, first_overflow_page_vpid);if (rec.area_size == -1) return ER_FAILED; /* length-query failure */rec.data = (char *) db_private_alloc (thread_p, rec.area_size); /* NULL -> ER_OUT_OF_VIRTUAL_MEMORY */if (overflow_get (thread_p, first_overflow_page_vpid, &rec, NULL) != S_SUCCESS) goto exit_on_error; /* NULL snapshot */or_init (&buf, rec.data, rec.length);pr_type->index_readval (&buf, key, btid->key_type, -1, true /*copy*/, NULL, 0); /* always copy overflow keys */비자명한 선택이 두 가지 있다. mvcc_snapshot = NULL은 지름길이 아니라 의미상 올바른 값이다. overflow key에는 MVCC 헤더가 없으며, 해당 key에 붙은 OID 리스트의 가시성이 접근 가능성을 결정한다. 그리고 index_readval (..., /*copy=*/true, ...)는 항상 복사하며 절대 peek하지 않는다. overflow_get은 반환 전에 페이지를 unfix하기 때문에, overflow key는 인라인 key처럼 리프 latch 하에서 peek할 수 없기 때문이다. 사전 크기 계산 덕분에 이 overflow_get은 S_DOESNT_FIT을 반환하지 않는다.
4.8 overflow_traverse와 overflow_flush — 워커 스켈레톤
섹션 제목: “4.8 overflow_traverse와 overflow_flush — 워커 스켈레톤”삭제(6장)와 플러시가 overflow_traverse를 공유한다. 여기서는 OVERFLOW_DO_FLUSH 분기만 다룬다.
// overflow_traverse -- src/storage/overflow_file.c (condensed)next_vpid = *ovf_vpid;while (!(VPID_ISNULL (&next_vpid))) { pgptr = pgbuf_fix (thread_p, &next_vpid, OLD_PAGE, PGBUF_LATCH_WRITE, PGBUF_UNCONDITIONAL_LATCH); if (pgptr == NULL) goto exit_on_error; vpid = next_vpid; overflow_next_vpid (ovf_vpid, &next_vpid, pgptr); /* read link BEFORE acting on page */ switch (func) { case OVERFLOW_DO_DELETE: ... /* needs ovf_vfid != NULL; deallocates page -- Chapter 6 */ case OVERFLOW_DO_FLUSH: if (overflow_flush_internal (thread_p, pgptr) != NO_ERROR) goto exit_on_error; break; default: break; } }return ovf_vpid;순회는 페이지를 콜백에 넘기기 전에 overflow_next_vpid로 다음 링크를 읽는다 — 삭제 시 페이지가 곧 해제되므로 필수적이며, 두 분기 모두에 일관되게 적용된다. overflow_flush_internal은 단일 분기(pgbuf_flush_with_wal, WAL: 페이지보다 로그 먼저)이고, overflow_flush는 overflow_traverse (..., NULL, ovf_vpid, OVERFLOW_DO_FLUSH)를 호출하는 void 래퍼다. 플러시임에도 PGBUF_LATCH_WRITE로 페이지를 고정하며, DELETE 분기의 ovf_vfid != NULL 가드는 실행되지 않는다. exit_on_error: /* TODO: suspect pgbuf_unfix */는 알려진 미완성 부분이다. 마지막으로 고정된 페이지의 fix가 누수될 수 있다(companion Cross-check).
4.9 overflow_get_capacity — 전체 진단 순회
섹션 제목: “4.9 overflow_get_capacity — 전체 진단 순회”Capacity 순회는 next_vpid를 NULL까지 따라가는 대신 length를 기준으로 모든 페이지를 순회하며, 크기, 페이지 수, 헤더 오버헤드, 말미 여유 공간을 보고한다.
// overflow_get_capacity -- src/storage/overflow_file.c (condensed)*ovf_size = remain_length = first_part->length; /* size output = total length */*ovf_num_pages = *ovf_overhead = *ovf_free_space = 0; /* other three outputs start zeroed */hdr_length = offsetof (OVERFLOW_FIRST_PART, data); next_vpid = first_part->next_vpid; /* head header larger */while (remain_length > 0) { if (remain_length > DB_PAGESIZE) remain_length -= DB_PAGESIZE - hdr_length; /* not last page: subtract payload capacity */ else { *ovf_free_space = DB_PAGESIZE - remain_length; remain_length = 0; } /* last page: tail is free */ *ovf_num_pages += 1; *ovf_overhead += hdr_length; if (remain_length > 0) /* unfix; same VPID_ISNULL guard + fix as 4.4; then: */ { hdr_length = offsetof (OVERFLOW_REST_PART, data); next_vpid = ((OVERFLOW_REST_PART *) pgptr)->next_vpid; } }pgbuf_unfix_and_init (thread_p, pgptr);세 분기(마지막 아닌 페이지 / 마지막 페이지 / 다음 페이지 fix)는 모두 인라인으로 주석 처리되어 있다. 다음 페이지 fix는 4.4의 VPID_ISNULL 가드와 fix 실패 종료 경로를 공유한다. 두 번째 반복부터 hdr_length가 헤드→rest로 전환되어 오버헤드 계산이 정확하게 유지된다. exit_on_error에서는 출력 네 개를 모두 0으로 초기화하여 부분 순회 결과가 반만 계산된 capacity로 보고되는 일이 없도록 한다. overflow_get_nbytes와 마찬가지로, capacity는 링크 체인보다 length를 신뢰한다.
4.10 챕터 요약 — 핵심 정리
섹션 제목: “4.10 챕터 요약 — 핵심 정리”length는 헤드에만 존재한다 —overflow_get_length는 fix-읽기-unfix 한 번으로 끝나고, 다른 모든 리더는length를 처음에 한 번만 읽는다.- 헤드와 rest의 구별은 VPID 동등성(
VPID_EQ)이며 플래그가 아니다. 인라인 리더는 루프 전에 헤드를 처리하고, 루프 내의 모든 페이지는 rest 페이지다. - MVCC 게이트는
TOO_OLD_FOR_SNAPSHOT에서만 중단한다 —TOO_NEW는SELECT ... FOR UPDATE가 방금 갱신된 big 레코드를 lock할 수 있게 통과시킨다.mvcc_snapshot == NULL이면(overflow key) 스킵된다. - 버퍼 크기 부족은 대역 외 신호다 —
S_DOESNT_FIT+ 음수recdes->length.heap_get_bigone_content는 루프로 크기를 늘리고,btree_load_overflow_key는 사전 크기 계산으로 이 상황을 회피한다. - 한 번에 하나의 페이지만 latch된다(unfix-before-fix). 체인 안정성은 홈 레퍼런스의 lock에 위임한다.
length바이트를 소비하기 전에next_vpid가 NULL이면 데이터 손상이다 — 두 리더 모두ER_HEAP_OVFADDRESS_CORRUPTED를 발생시킨다.overflow_flush는overflow_traverse의 무해한 분기다 (페이지당pgbuf_flush_with_wal, VFID 없음). DELETE는 6장에서 다룬다.
Chapter 5: overflow_update로 체인을 제자리에서 갱신하기
섹션 제목: “Chapter 5: overflow_update로 체인을 제자리에서 갱신하기”독자의 질문: 새 값이 기존 저장된 값보다 길거나 짧게 재작성될 때, overflow_insert(Chapter 3)나 overflow_delete(Chapter 6)처럼 체인을 통째로 헐고 재구성하지 않으면서, 기존 체인을 페이지 단위로 늘리거나 줄이는 방법은 무엇인가?
답은 overflow_update다. 이 함수는 체인의 헤드부터 테일까지 순차적으로 걸어가며 각 페이지의 페이로드를 제자리에 덮어쓰고, 새 길이가 소진되거나 초과되는 전환 지점에서만 토폴로지(next_vpid 링크, 페이지 할당)를 건드린다. 이 챕터는 모든 분기를 해부한다. OVERFLOW_FIRST_PART / OVERFLOW_REST_PART 필드는 Chapter 1에서 다룬다.
5.1 힙만 이 함수를 호출하는 이유 — FILE_MULTIPAGE_OBJECT_HEAP assert
섹션 제목: “5.1 힙만 이 함수를 호출하는 이유 — FILE_MULTIPAGE_OBJECT_HEAP assert”overflow_update는 호출자를 제한하는 두 개의 assert로 시작한다:
// overflow_update -- src/storage/overflow_file.cassert (ovf_vfid != NULL && !VFID_ISNULL (ovf_vfid));/* used only for heap for now... this doesn't consider temporary files. -- author's comment, condensed */assert (file_type == FILE_MULTIPAGE_OBJECT_HEAP); /* <- in-place update is a heap-only path */B+Tree OID 리스트 변형(Chapter 7)은 제자리 갱신을 수행하지 않는다. 키/OID 변경은 항상 삭제 후 재삽입이다. 오직 힙의 REC_BIGONE 레코드만이 heap_ovf_update를 거쳐 overflow_update에 도달한다. 이 assert는 계약을 코드화한 것이다: 이 함수는 임시 파일(다른 페이지 초기화 함수가 필요)을 처리하지 않으며, 영구 힙 overflow 타입에 대해서만 검증되었다. 구조적 귀결로서 헤드 페이지는 절대 해제되지 않는다 — 5.5절 참조.
5.2 heap_ovf_update — 얇은 래퍼
섹션 제목: “5.2 heap_ovf_update — 얇은 래퍼”heap_ovf_update는 힙의 OID 세계를 VFID/VPID로 변환하는 역할을 한다:
// heap_ovf_update -- src/storage/heap_file.cif (heap_ovf_find_vfid (thread_p, hfid, &ovf_vfid, false, PGBUF_UNCONDITIONAL_LATCH) == NULL) return NULL; /* <- could not resolve overflow file VFID */ovf_vpid.pageid = ovf_oid->pageid;ovf_vpid.volid = ovf_oid->volid; /* <- head VPID lives in the OID */if (overflow_update (thread_p, &ovf_vfid, &ovf_vpid, recdes, FILE_MULTIPAGE_OBJECT_HEAP) != NO_ERROR) { ASSERT_ERROR (); return NULL; } /* <- propagate failure as NULL */else return ovf_oid; /* <- OID unchanged: head never moved */heap_ovf_find_vfid에 false를 넘기는 점에 주목하자 — 파일을 새로 만들지 말고 이미 존재해야 한다는 의미다. 성공 시 동일한 ovf_oid가 반환된다. 헤드가 불변이므로 relocation 레코드를 다시 쓸 필요가 없다. 호출자는 heap_update_physical의 REC_BIGONE -> REC_BIGONE 분기이며, 새 이미지에 대해서도 heap_is_big_length가 여전히 참일 때만 이 경로가 실행된다.
체인 토폴로지(REC_BIGONE 슬롯 → 헤드 OVERFLOW_FIRST_PART → OVERFLOW_REST_PART … → NULL 테일)는 Chapter 1의 자료구조 지도다. 제자리 갱신은 각 페이지의 data[]를 변경하고 선택적으로 next_vpid를 재기록하지만, REC_BIGONE 슬롯의 헤드 VPID는 절대 바뀌지 않는다.
5.3 메인 루프 골격과 반복 간 상태
섹션 제목: “5.3 메인 루프 골격과 반복 간 상태”본체는 sysop으로 감싼 하나의 while (length > 0) 루프다:
// overflow_update -- src/storage/overflow_file.cnext_vpid = *ovf_vpid; /* <- start at the head */data = recdes->data;length = recdes->length; /* <- bytes still to write */log_sysop_start (thread_p); /* <- atomic bracket; see 5.6 */
while (length > 0) { addr.pgptr = pgbuf_fix (thread_p, &next_vpid, OLD_PAGE, PGBUF_LATCH_WRITE, PGBUF_UNCONDITIONAL_LATCH); if (addr.pgptr == NULL) { ASSERT_ERROR_AND_SET (error_code); goto exit_on_error; } addr_vpid_ptr = pgbuf_get_vpid_ptr (addr.pgptr); /* ... first-page branch vs rest-page branch ... */ }네 개의 지역 변수가 반복 간 상태를 유지하며 분기를 선택한다:
| 지역 변수 | 역할 | 존재 이유 |
|---|---|---|
length | 아직 쓰지 않은 새 이미지 바이트 수 | 루프 가드; 0이 되면 종료/축소 경로를 트리거 |
old_length | undo before-image로 아직 커버되지 않은 기존 이미지 바이트 수 | 페이지당 로깅할 before-image 크기를 결정 |
isnewpage | 이번 갱신에서 file_alloc으로 페이지를 할당했으면 true | rest 분기가 새 페이지를 NULL-종료해야 함을 알림 |
rest_parts | 헤드를 지난 뒤 non-NULL | 성장/종료 시 first_part vs rest_parts 중 어디에 링크할지 결정 |
before-image 분할 규칙 (5.4 전반에서 참조). old_length는 헤드에서 한 번만 first_part->length로부터 읽힌다. 기존 페이로드를 아직 담고 있는 각 페이지에서 코드는 RVOVF_PAGE_UPDATE undo를 로깅하고, 해당 페이지가 보유하던 만큼 old_length를 감소시킨다 — rest가 넘치면 전체 DB_PAGESIZE, 아니면 정확히 hdr_length + old_length (그 후 old_length = 0). 기존 바이트 수를 로깅하는 것이 undo 시 원래 페이지를 정확히 복원할 수 있게 하며, 페이지마다 추적함으로써 더 짧은 새 이미지도 더 긴 기존 이미지를 복원할 수 있다. 5.4의 두 분기 모두 이 규칙을 그대로 사용하며, 헤드는 추가로 old_length를 초기화한다.
5.4 루프 내부: 두 가지 페이지 분기
섹션 제목: “5.4 루프 내부: 두 가지 페이지 분기”VPID_EQ (addr_vpid_ptr, ovf_vpid)가 방금 fix한 페이지가 헤드인지를 판별한다.
5.4.1 첫 번째 페이지(헤드) 분기
섹션 제목: “5.4.1 첫 번째 페이지(헤드) 분기”// overflow_update (first-page branch) -- src/storage/overflow_file.cfirst_part = (OVERFLOW_FIRST_PART *) addr.pgptr;old_length = first_part->length; /* <- whole old image length, read once (seeds 5.3 rule) */copyto = (char *) first_part->data;next_vpid = first_part->next_vpid; /* <- remember existing successor */hdr_length = offsetof (OVERFLOW_FIRST_PART, data);
/* copy_length = min(length, DB_PAGESIZE - hdr_length): fill page if more follows, else exact fit */if ((length + hdr_length) > DB_PAGESIZE) copy_length = DB_PAGESIZE - hdr_length;else copy_length = length;
/* Before-image, logged unconditionally (head always held payload) -- the 5.3 split: */if (hdr_length + old_length > DB_PAGESIZE) /* old image spills past the head */ { log_append_undo_data (thread_p, RVOVF_PAGE_UPDATE, &addr, DB_PAGESIZE, addr.pgptr); old_length -= DB_PAGESIZE - hdr_length; } /* <- one full page of old payload consumed */else /* old image fits on the head: log hdr_length+old_length bytes, then old_length = 0 */ { log_append_undo_data (thread_p, RVOVF_PAGE_UPDATE, &addr, hdr_length + old_length, addr.pgptr); old_length = 0; }
first_part->length = length; /* <- store the NEW total length on the head */log_append_empty_record (thread_p, LOG_DUMMY_OVF_RECORD, &addr);first_part->length는 새 전체 길이가 된다(length는 오직 헤드만 보관한다). 뒤따르는 LOG_DUMMY_OVF_RECORD는 복구/스캔이 새로 스탬프된 overflow 헤드를 인식하는 데 사용하는 마커를 재발행한다 — redo 시 이것이 중요한 이유는 Chapter 8에서 다룬다.
5.4.2 나머지 페이지(rest) 분기
섹션 제목: “5.4.2 나머지 페이지(rest) 분기”// overflow_update (rest-page branch) -- src/storage/overflow_file.crest_parts = (OVERFLOW_REST_PART *) addr.pgptr;copyto = (char *) rest_parts->data;if (isnewpage == true) { VPID_SET_NULL (&next_vpid); rest_parts->next_vpid = next_vpid; /* <- a page WE just allocated terminates the chain */ }else next_vpid = rest_parts->next_vpid; /* <- pre-existing page: keep following the old links */
hdr_length = offsetof (OVERFLOW_REST_PART, data);/* copy_length computed exactly as in 5.4.1 (fill page vs fits) */
if (old_length > 0) /* <- only log undo if old payload still reaches here */ { /* ... identical before-image split as 5.3 / 5.4.1 ... */ }isnewpage가 성장 경로를 제어한다. 5.4.3에서 방금 할당된 페이지는 next_vpid에 쓰레기 값을 담고 있으므로, 이 분기는 NULL로 종료한다(성장 도중 크래시가 발생해도 초기화되지 않은 링크를 따라가지 않게 된다). 기존 페이지라면 next_vpid를 읽어 기존 토폴로지를 따른다. if (old_length > 0) 가드는 축소 대응 로직이다 — 이전 before-image들이 모든 기존 바이트를 이미 커버했다면, 곧 해제될 테일 페이지들의 undo는 로깅하지 않는다. 링크 opcode와 file_dealloc이 그것을 감당한다.
5.4.3 공통 페이로드 복사, 그 후 성장 또는 종료
섹션 제목: “5.4.3 공통 페이로드 복사, 그 후 성장 또는 종료”두 분기에서 copyto/copy_length/hdr_length가 설정된 후:
// overflow_update (shared) -- src/storage/overflow_file.cmemcpy (copyto, data, copy_length); /* <- overwrite payload in place */data += copy_length; length -= copy_length;if (thread_p->no_logging != true) log_append_redo_data (thread_p, RVOVF_PAGE_UPDATE, &addr, copy_length + hdr_length, (char *) addr.pgptr);redo는 copy_length + hdr_length 바이트 — 헤더 + 새 페이로드 — 를 로깅하여 after-image로부터 갱신 후 페이지를 재구성한다. (no_logging은 벌크 로드 고속 경로로, redo만 억제한다.) 그 후 체인 토폴로지를 결정하는 분기가 이어진다:
// overflow_update (grow path) -- src/storage/overflow_file.cif (length > 0) { if (VPID_ISNULL (&next_vpid)) /* <- ran out of existing pages: GROW */ { error_code = file_alloc (thread_p, ovf_vfid, file_init_page_type, &ptype, &next_vpid, NULL); if (error_code != NO_ERROR) { ASSERT_ERROR (); pgbuf_set_dirty_and_free (thread_p, addr.pgptr); goto exit_on_error; }
log_append_undoredo_data (thread_p, RVOVF_NEWPAGE_LINK, &addr, 0, sizeof (next_vpid), NULL, &next_vpid); isnewpage = true; /* <- next iteration NULL-terminates the new page */
if (rest_parts == NULL) first_part->next_vpid = next_vpid; /* <- still on head: link grows from first_part */ else rest_parts->next_vpid = next_vpid; /* <- link grows from current rest page */ } pgbuf_set_dirty_and_free (thread_p, addr.pgptr); }성장은 페이로드가 남아 있고 next_vpid가 NULL일 때만 실행된다. RVOVF_NEWPAGE_LINK의 undoredo는 undo 길이가 0이다 — 새 링크의 undo는 “링크 없음”이며, file_alloc 자체의 레코드가 롤백 시 페이지를 회수한다. 새 VPID는 first_part(아직 헤드에 있을 때) 또는 rest_parts로부터 링크된다. next_vpid가 NULL이 아니면 할당을 건너뛰고 기존 후계 페이지로 진행한다. file_alloc 실패 시 래치 누수 없이 페이지를 dirty-and-free한 뒤 goto exit_on_error로 이동하고, sysop abort가 롤백을 처리한다(5.6절).
5.4.4 종료 및 축소 경로
섹션 제목: “5.4.4 종료 및 축소 경로”// overflow_update (terminate/shrink) -- src/storage/overflow_file.celse /* length == 0 : no more payload */ { VPID_SET_NULL (&tmp_vpid); log_append_undoredo_data (thread_p, RVOVF_CHANGE_LINK, &addr, sizeof (next_vpid), sizeof (next_vpid), &next_vpid, &tmp_vpid); /* <- undo restores old next_vpid; redo sets NULL */
if (rest_parts == NULL) VPID_SET_NULL (&first_part->next_vpid); /* <- single-page result: head's link cut */ else VPID_SET_NULL (&rest_parts->next_vpid); /* <- cut after the last written rest page */ pgbuf_set_dirty_and_free (thread_p, addr.pgptr);
while (!(VPID_ISNULL (&next_vpid))) /* <- free every surplus tail page */ { addr.pgptr = pgbuf_fix (...); // ... fix; NULL -> exit_on_error ... tmp_vpid = next_vpid; rest_parts = (OVERFLOW_REST_PART *) addr.pgptr; next_vpid = rest_parts->next_vpid; /* <- read successor BEFORE unfixing */ pgbuf_unfix_and_init (thread_p, addr.pgptr); error_code = file_dealloc (thread_p, ovf_vfid, &tmp_vpid, file_type); if (error_code != NO_ERROR) { ASSERT_ERROR (); goto exit_on_error; } } break; /* <- leave the while(length>0) loop */ }이 분기는 length == 0일 때 한 번 실행된다. RVOVF_CHANGE_LINK는 기존 next_vpid를 undo로, NULL을 redo로 로깅한다. 링크는 마지막으로 쓰인 페이지(rest_parts가 NULL이면 헤드, 즉 값이 한 페이지에 맞으면 헤드; 아니면 마지막 rest 페이지)에서 NULL로 끊긴다. 이후 테일 해제 루프가 각 잉여 페이지를 fix하고, unfix 전에 next_vpid를 미리 읽어 체인이 해제 과정에서 끊기지 않도록 한 뒤, unfix하고 file_dealloc한다. NULL에서 루프가 종료되면 break로 외부 루프를 빠져나온다. 실패 시에는 sysop abort로 점프한다.
Figure 5-1은 이 함수의 흐름을 추적한다.
flowchart TD
S["sysop 시작; 헤드에서 출발"] --> L{"length > 0 ?"}
L -- 아니오 --> ATT["외부 트랜잭션에 attach; NO_ERROR 반환"]
L -- 예 --> FIX["next_vpid WRITE fix"]
FIX -- NULL --> ERR["exit_on_error"]
FIX --> HEAD{"VPID_EQ 헤드 ?"}
HEAD -- 예 --> FP["첫 번째 페이지 분기"]
HEAD -- 아니오 --> RP["나머지 페이지 분기"]
FP --> CP["memcpy; redo after-image 로깅"]
RP --> CP
CP --> M{"length > 0 ?"}
M -- 예 --> G{"next_vpid NULL ?"}
G -- 예 --> ALLOC["성장: file_alloc; 링크"]
G -- 아니오 --> DF["후계 재사용; dirty+free; 루프"]
ALLOC --> DF
DF --> L
M -- 아니오 --> TERM["종료: 링크 절단"]
TERM --> TL{"next_vpid NULL ?"}
TL -- 아니오 --> DEA["축소: 테일 페이지 해제"]
DEA --> TL
TL -- 예 --> ATT
ERR --> AB["sysop abort; 오류 반환"]
Figure 5-1. overflow_update 제어 흐름 — 모든 분기와 오류 출구.
5.5 헤드는 절대 제거되지 않는다는 불변 조건
섹션 제목: “5.5 헤드는 절대 제거되지 않는다는 불변 조건”불변 조건: 힙 overflow 체인은 해당 값이 살아 있는 동안 항상 헤드 페이지(OVERFLOW_FIRST_PART)를 유지한다. overflow_update는 이를 구조적으로 강제한다. 헤드는 가장 먼저 fix되고 항상 앞부분 copy_length 바이트를 수신하며, 5.4.4의 테일 해제 루프는 마지막으로 쓰인 페이지 이후(즉 next_vpid부터 시작)에 있는 페이지만 해제한다 — 헤드는 절대 해제 대상이 되지 않는다. 이미지가 몇 바이트로 줄어들더라도 헤드는 next_vpid가 NULL로 설정된 채 살아남는다. 축소가 헤드를 해제할 수 있다면, REC_BIGONE OID가 허상 포인터(dangling pointer)가 되어 그것을 resolve하려는 모든 독자(Chapter 4)가 오류를 낼 것이다 — 헤드를 고정하는 것이 heap_ovf_update가 변경되지 않은 ovf_oid를 반환할 수 있는 근거다.
5.6 시스템 오퍼레이션 브래킷
섹션 제목: “5.6 시스템 오퍼레이션 브래킷”갱신 전체는 하나의 중첩 원자 단위다:
// overflow_update -- src/storage/overflow_file.clog_sysop_start (thread_p);/* ... while loop, allocations, deallocations ... */log_sysop_attach_to_outer (thread_p); return NO_ERROR; /* success: splice into enclosing txn */exit_on_error:log_sysop_abort (thread_p); return error_code; /* failure: undo writes, alloc, dealloc */성공 시 sysop은 독립적으로 커밋되지 않고 외부 트랜잭션에 attach된다 — 모든 페이지 재작성, 링크 변경, file_alloc/file_dealloc이 상위 힙 갱신의 일부가 된다. 어떤 오류(pgbuf_fix NULL, file_alloc/file_dealloc 실패)가 발생하든 goto exit_on_error가 log_sysop_abort를 실행하여 지금까지 로깅된 undo를 재생하고, 체인을 이전 상태 그대로 남긴다. 5.4에서 언급된 각 opcode가 복구 시 어떻게 적용되는지는 Chapter 8에서 자세히 다룬다.
5.7 챕터 요약 — 핵심 정리
섹션 제목: “5.7 챕터 요약 — 핵심 정리”- 제자리 갱신은 힙 전용이다.
assert (file_type == FILE_MULTIPAGE_OBJECT_HEAP)이 다른 호출자를 차단한다. B+Tree 값 변경은 삭제 후 재삽입이다. - 걷기(walk)는
memcpy로 페이지별 페이로드를 덮어쓰며, 페이지마다RVOVF_PAGE_UPDATEundo(before-image)와 redo(after-image)를 로깅한다.length는 헤드만 보관한다. old_length가 undo를 주도한다. 5.3의 단일 분할 규칙(헤드에서 초기화, 페이지마다 감소)에 따른다.if (old_length > 0)가드는 소멸 예정 테일 페이지의 undo를 억제하므로, 더 짧은 새 이미지도 원본을 복원할 수 있다.- 성장은 페이로드가 남아 있고
next_vpid가 NULL일 때만 페이지를 할당하며(file_alloc+RVOVF_NEWPAGE_LINK),isnewpage를 설정하고first_part또는rest_parts로부터 링크한다. - 축소/종료는
length == 0일 때 한 번 실행된다.RVOVF_CHANGE_LINK가 후미 링크를 끊고, 테일 루프가 각 잉여 페이지를 fix → 선독(next_vpid) → unfix →file_dealloc순으로 처리한다. - 헤드는 절대 해제되지 않으므로(5.5절),
REC_BIGONEOID는 유효한 상태를 유지하고heap_ovf_update는 변경되지 않은 OID를 반환한다. - 전체 오퍼레이션은 하나의 sysop이다. 성공 시
log_sysop_attach_to_outer, 모든 오류 경로에서log_sysop_abort— 상위 트랜잭션과 원자적으로 연결된다.
Chapter 6: 체인 삭제와 페이지 반환
섹션 제목: “Chapter 6: 체인 삭제와 페이지 반환”큰 레코드 값이 완전히 사라질 때, overflow 체인의 각 페이지는 어떻게 해제되며, MVCC 하에서는 누가 그 방아쇠를 당기는가? 물리적 반환 자체는 간결하다 — overflow_delete → overflow_traverse(OVERFLOW_DO_DELETE) → 페이지별 file_dealloc. 흥미로운 부분은 호출자 쪽이다. REC_BIGONE에 대한 MVCC 삭제는 체인을 해제하지 않는다. MVCC 삭제는 head overflow 페이지에 delete-MVCCID를 기록하고 home heap 슬롯을 살아있는 REC_BIGONE인 채로 내버려 두므로, vacuum(또는 비-MVCC 삭제)이 해당 행을 더 이상 아무도 볼 수 없다고 판단할 때까지 체인이 유지된다. 체인 구조는 Chapter 1, 구성은 Chapter 3, 페이지별 락-없음 근거는 companion 문서(cubrid-overflow-file.md)를 참고하라.
6.1 반환 핵심 — overflow_delete와 순회
섹션 제목: “6.1 반환 핵심 — overflow_delete와 순회”overflow_delete는 OVERFLOW_DO_DELETE 판별자를 넘기며 공용 순회-콜백 함수 overflow_traverse에 작업을 위임한다(OVERFLOW_DO_FLUSH에도 같은 함수가 쓰인다). 이 함수는 각 페이지를 fix한 뒤, 현재 페이지에 작업하기 전에 next 링크를 먼저 읽는다:
// overflow_traverse -- src/storage/overflow_file.c (condensed)next_vpid = *ovf_vpid;while (!(VPID_ISNULL (&next_vpid))) { pgptr = pgbuf_fix (thread_p, &next_vpid, OLD_PAGE, PGBUF_LATCH_WRITE, PGBUF_UNCONDITIONAL_LATCH); if (pgptr == NULL) { goto exit_on_error; } /* <- fix 실패: NULL 반환 */ vpid = next_vpid; overflow_next_vpid (ovf_vpid, &next_vpid, pgptr); /* <- 해제 전에 링크 읽기 */ switch (func) { case OVERFLOW_DO_DELETE: if (ovf_vfid == NULL) /* assert: dealloc에는 VFID 필요 */ { goto exit_on_error; } if (overflow_delete_internal (thread_p, ovf_vfid, &vpid, pgptr) != NO_ERROR) { goto exit_on_error; } break; case OVERFLOW_DO_FLUSH: /* ... condensed ... */ break; default: break; } }return ovf_vpid;실제 switch (func)에는 세 개의 분기가 있다 — OVERFLOW_DO_DELETE, OVERFLOW_DO_FLUSH(overflow_flush_internal 호출), 그리고 default: break; 무동작 — 하지만 삭제 순회에서 사용되는 것은 첫 번째뿐이다. 세 가지 오류 경로는 모두 exit_on_error → NULL으로 수렴한다(Figure 6-1): pgbuf_fix NULL(탈출; free는 커밋 이후의 postpone이므로 abort는 무해함, §6.2), ovf_vfid == NULL 단언(소유 파일 없이 dealloc 불가 — overflow_flush가 의도적으로 NULL을 넘기기 때문에 분기별로 처리), overflow_delete_internal 실패(전파). 네 번째 경우인 next_vpid NULL 루프 종료는 오류 경로가 아니다. while을 빠져나와 성공 센티넬인 return ovf_vpid로 이어진다. exit_on_error 자체는 unfix를 수행하지 않는다(/* TODO: suspect pgbuf_unfix */ 주석 참조). 따라서 루프 바디에서 fix에 성공한 후 오류가 발생하는 경우 페이지가 누수된다.
불변 조건 — “현재 페이지를 해제하기 전에 next-page 링크를 읽어라.”
overflow_traverse는overflow_next_vpid를overflow_delete_internal이 unfix·dealloc하기 전에 호출한다. 순서가 뒤바뀌면 해제된 페이지에서 next 링크를 역참조하게 되어 — use-after-free로 체인 꼬리가 손상된다.
(overflow_next_vpid는 타입을 인식한다 — head에서는 OVERFLOW_FIRST_PART로, 그 외에는 OVERFLOW_REST_PART로 캐스팅한다 — 하지만 두 구조체 모두 next_vpid를 같은 앞쪽 오프셋에 두므로 캐스팅은 형식적이다.)
flowchart TD
A["overflow_delete(ovf_vfid, ovf_vpid)"] --> B["overflow_traverse(..., OVERFLOW_DO_DELETE)"]
B --> C{"next_vpid가 NULL?"}
C -- "예" --> Z["return ovf_vpid (성공)"]
C -- "아니오" --> D["pgbuf_fix(next_vpid, WRITE)"]
D --> E{"pgptr == NULL?"}
E -- "예" --> X["exit_on_error -> NULL 반환"]
E -- "아니오" --> F["vpid := next_vpid\noverflow_next_vpid -> next_vpid 전진"]
F --> G{"ovf_vfid == NULL?"}
G -- "예" --> X
G -- "아니오" --> H["overflow_delete_internal(ovf_vfid, vpid, pgptr)"]
H --> I{"NO_ERROR?"}
I -- "아니오" --> X
I -- "예" --> C
Figure 6-1 — overflow_delete 순회: 페이지 fix, next-link 수집, dealloc; 오류 분기는 exit_on_error로 수렴.
6.2 페이지별 반환 — overflow_delete_internal과 FILE_UNKNOWN_TYPE TODO
섹션 제목: “6.2 페이지별 반환 — overflow_delete_internal과 FILE_UNKNOWN_TYPE TODO”페이지별 작업 함수는 간결하다 — unfix 후 file_dealloc:
// overflow_delete_internal -- src/storage/overflow_file.c pgbuf_unfix_and_init (thread_p, pgptr); /* <- dealloc 전에 latch+pin 해제 */ /* TODO: clarify file_type */ ret = file_dealloc (thread_p, ovf_vfid, vpid, FILE_UNKNOWN_TYPE); /* <- hint = UNKNOWN */ if (ret != NO_ERROR) { goto exit_on_error; } return ret;exit_on_error: return (ret == NO_ERROR && (ret = er_errid ()) == NO_ERROR) ? ER_FAILED : ret;페이지는 file_dealloc 전에 unfix된다. next 링크는 이미 수집되어 있으므로 write-latch 경합이 없다 — 체인은 레코드에 대한 상위 레벨 락이 독점적이므로 페이지별 락은 필요하지 않다(companion 문서 참조).
FILE_UNKNOWN_TYPE 힌트와 그 TODO. 세 번째 인자는 힌트다. FILE_UNKNOWN_TYPE을 넘기면 file_dealloc이 파일 헤더를 fix해서 실제 타입을 읽어온다(file_type_hint = fhead->type). #if defined (NDEBUG)는 if 조건만을 감싸기 때문에, 헤더-fix 분기의 도달 가능성은 빌드 모드에 따라 달라진다:
// file_dealloc -- src/storage/file_manager.c#if defined (NDEBUG) if (file_type_hint == FILE_UNKNOWN_TYPE || file_type_hint == FILE_EXTENDIBLE_HASH || file_type_hint == FILE_EXTENDIBLE_HASH_DIRECTORY)#endif /* NDEBUG */ { /* this should be avoided in release */ page_fhead = pgbuf_fix (thread_p, &vpid_fhead, OLD_PAGE, PGBUF_LATCH_WRITE, PGBUF_UNCONDITIONAL_LATCH); fhead = (FILE_HEADER *) page_fhead; assert (file_type_hint == FILE_UNKNOWN_TYPE || file_type_hint == fhead->type /* ... */); file_type_hint = fhead->type; /* <- hint가 실제 타입으로 교체됨 */ }릴리스 빌드(NDEBUG)에서는 이 분기가 조건부 — FILE_UNKNOWN_TYPE과 extendible-hash 두 타입에 대해서만 실행된다. 디버그 빌드에서는 #if가 조건 자체를 제거하므로 분기가 무조건 실행된다 — 모든 file_dealloc이 헤더를 읽어 힌트를 검증한다. 어느 경우든 overflow dealloc은 항상 FILE_UNKNOWN_TYPE을 넘기므로 헤더 fix는 항상 여기서 발생한다. 따라서 이 TODO는 정확성과 무관한 정리 작업이다. 호출자는 타입을 알고 있지만(heap은 FILE_MULTIPAGE_OBJECT_HEAP, B+Tree 키는 FILE_BTREE_OVERFLOW_KEY) 그것을 아래로 넘기지 않는다. companion 문서 Open Questions §2를 참조하라.
페이지별 RVOVF_* undo/redo 레코드가 생성되지 않는 이유. 삭제 시 overflow 전용 로그 레코드는 전혀 생성되지 않으며, 복구는 file_dealloc에 위임된다 — 모든 파일에 대한 RVFL_DEALLOC postpone, 그리고 numerable/user-page-table 분기에서만 RVFL_USER_PAGE_MARK_DELETE. RVFL_DEALLOC은 postpone 레코드이므로, 영구 파일의 페이지는 run postpone(커밋 이후)에서만 해제된다. 따라서 abort 시 되돌릴 필요가 없으며, §6.1의 오류 경로에서 롤백 없이 NULL을 반환하는 것이 안전하다. RVOVF_* 레코드는 중복이 된다.
6.3 heap MVCC 비대칭 — MVCC 삭제는 체인을 해제하지 않는다
섹션 제목: “6.3 heap MVCC 비대칭 — MVCC 삭제는 체인을 해제하지 않는다”heap_delete_bigone은 is_mvcc_op를 기준으로 분기한다. MVCC 경로에서는 heap_ovf_delete를 절대 호출하지 않는다 — overflow 페이지에서 MVCC 헤더를 읽어 delete-MVCCID를 기록하고, home 슬롯은 살아있는 REC_BIGONE 상태로 유지한다:
// heap_delete_bigone -- src/storage/heap_file.c (is_mvcc_op branch, condensed)overflow_oid = *((OID *) context->home_recdes.data); /* home 슬롯에는 OID만 있음 *//* ... fix overflow page ... */if (heap_get_mvcc_rec_header_from_overflow (..., &overflow_header, NULL) != NO_ERROR) { return ER_FAILED; }heap_mvcc_log_delete (thread_p, &log_addr, RVHF_MVCC_DELETE_OVERFLOW);heap_delete_adjust_header (&overflow_header, mvcc_id, false); /* <- DELID 기록 */heap_set_mvcc_rec_header_on_overflow (..., &overflow_header); /* overflow 페이지에 씀 */heap_mvcc_log_home_no_change (thread_p, &log_addr); /* <- home 슬롯 변경 없음 */heap_delete_adjust_header가 실제 기록을 수행한다(MVCC_SET_FLAG_BITS(..., OR_MVCC_FLAG_VALID_DELID) 후 MVCC_SET_DELID). 세 번째 인자 need_mvcc_header_max_size가 false인 이유는 헤더가 이미 최대 크기이기 때문이다(§6.4). 대칭 함수 heap_get_mvcc_rec_header_from_overflow는 head 페이지 data[]의 첫 OR_MVCC_MAX_HEADER_SIZE 바이트를 읽는다.
불변 조건 — “큰 레코드에 대한 MVCC 삭제는 head overflow 페이지만 변경하며, home heap 슬롯은 살아있는
REC_BIGONE으로 유지된다.”is_mvcc_op분기에 의해 강제된다. 유일한 쓰기는 overflow 페이지에 대한heap_set_mvcc_rec_header_on_overflow와, home 페이지의 max-MVCCID/vacuum-status를 갱신해 vacuum이 재방문하도록 하는 no-change 레코드(heap_mvcc_log_home_no_change)뿐이다. 이 시점에 체인을 해제하면,mvcc_id보다 이전 스냅샷을 가진 reader가 해제된 overflow 페이지를 fix하게 되어 — 가시성 오류가 발생한다.
6.4 강제 최대 헤더 불변 조건 — heap_set_mvcc_rec_header_on_overflow
섹션 제목: “6.4 강제 최대 헤더 불변 조건 — heap_set_mvcc_rec_header_on_overflow”인-플레이스 MVCC 삭제가 가능하려면, DELID를 기록할 때 헤더의 바이트 크기가 절대 변하지 않아야 한다. 이를 위해 CUBRID는 head overflow 페이지의 MVCC 헤더를 항상 최대 크기로 강제한다:
// heap_set_mvcc_rec_header_on_overflow -- src/storage/heap_file.c ovf_recdes.data = overflow_get_first_page_data (ovf_page); ovf_recdes.area_size = ovf_recdes.length = OR_HEADER_SIZE (ovf_recdes.data); assert (ovf_recdes.length == OR_MVCC_MAX_HEADER_SIZE); /* <- 크기 고정 */ if (!MVCC_IS_FLAG_SET (mvcc_header, OR_MVCC_FLAG_VALID_INSID)) /* 누락된 INSID 패딩 */ { MVCC_SET_FLAG_BITS (mvcc_header, OR_MVCC_FLAG_VALID_INSID); MVCC_SET_INSID (mvcc_header, MVCCID_ALL_VISIBLE); } if (!MVCC_IS_FLAG_SET (mvcc_header, OR_MVCC_FLAG_VALID_DELID)) /* 누락된 DELID 패딩 */ { 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);불변 조건 — “head overflow 페이지의 MVCC 헤더는 항상
OR_MVCC_MAX_HEADER_SIZE다.” 누락된 INSID는MVCCID_ALL_VISIBLE로, 누락된 DELID는MVCCID_NULL로 패딩하고 크기를 단언함으로써 강제된다. 이 덕분에 §6.3에서 DELID를 인-플레이스로 기록할 때 row 바디가 밀리지 않는다. 헤더가 짧으면 바디를 오염시키거나 읽어들일 때 잘못 파싱된다.
6.5 실제 해제 — 비-MVCC 삭제와 vacuum
섹션 제목: “6.5 실제 해제 — 비-MVCC 삭제와 vacuum”else 분기(비-MVCC 경로) — 비-MVCC 클래스 삭제 또는 §6.3의 delete-MVCCID가 모든 스냅샷을 넘어선 후 vacuum이 도달했을 때 실행된다 — 는 home 슬롯을 먼저 회수한 다음 체인을 해제한다:
// heap_delete_bigone -- src/storage/heap_file.c (non-MVCC branch, condensed)bool is_reusable = heap_is_reusable_oid (context->file_type);heap_log_delete_physical (..., &context->home_recdes, is_reusable, NULL); /* home-slot 로그 */rc = heap_delete_physical (thread_p, &context->hfid, ..., &context->oid); /* home 슬롯 해제 */if (rc != NO_ERROR) { return rc; }if (heap_ovf_delete (thread_p, &context->hfid, &overflow_oid, NULL) == NULL) /* <- 체인 해제 */ { return ER_FAILED; }heap_log_delete_physical은 RVHF_DELETE undoredo 레코드를 기록하고 — is_reusable에 따라 — RVHF_MARK_REUSABLE_SLOT postpone를 추가한다(home 슬롯의 운명 분기; 핵심 정리 6). heap_ovf_delete는 ovf_vfid_p == NULL로 호출되어 overflow 파일을 먼저 resolve한 뒤 위임한다:
// heap_ovf_delete -- src/storage/heap_file.c if (ovf_vfid_p == NULL || VFID_ISNULL (ovf_vfid_p)) /* <- 호출자가 NULL 전달 */ { ovf_vfid_p = (ovf_vfid_p != NULL) ? ovf_vfid_p : &ovf_vfid; if (heap_ovf_find_vfid (thread_p, hfid, ovf_vfid_p, false /*docreate*/, ...) == NULL) { return NULL; } /* <- ovf 파일을 찾지 못함 */ } ovf_vpid.pageid = ovf_oid->pageid; ovf_vpid.volid = ovf_oid->volid; /* OID 페이지 = head VPID */ if (overflow_delete (thread_p, ovf_vfid_p, &ovf_vpid) == NULL) { return NULL; } return ovf_oid;docreate = false — 삭제 시에는 파일을 생성하지 않는다. ovf_oid의 (volid, pageid)가 곧 체인 head VPID다. 분기: (1) ovf_vfid_p가 non-NULL이면 lookup 생략(vacuum의 캐시된 VFID 경로); (2) NULL 또는 null 값이면 heap_ovf_find_vfid로 resolve, 실패 시 NULL; (3)/(4) overflow_delete의 NULL/성공을 전파한다.
stateDiagram-v2 [*] --> Live: REC_BIGONE 삽입, 체인 할당 Live --> DelStamped: MVCC 삭제\nhead 페이지에 DELID 기록\nhome 슬롯 변경 없음 DelStamped --> Reclaimed: vacuum 또는 비-MVCC 삭제\nheap_ovf_delete -> overflow_delete\nhome 슬롯 -> REC_MARKDELETED 또는 REC_DELETED_WILL_REUSE Live --> Reclaimed: 비-MVCC 클래스 삭제\n직접 heap_ovf_delete 호출 Reclaimed --> [*]: run-postpone 시 페이지 file_dealloc
Figure 6-2 — 삭제 시점의 생명주기. MVCC 삭제는 행을 DelStamped로만 이동시킨다(체인 유지). 체인은 Reclaimed에서만 해제된다.
6.6 B+Tree overflow-key 삭제 — btree_delete_overflow_key
섹션 제목: “6.6 B+Tree overflow-key 삭제 — btree_delete_overflow_key”B+Tree overflow-key 경로(긴 키가 FILE_BTREE_OVERFLOW_KEY 체인으로 spill된 경우)는 MVCC 지연 없이 overflow_delete를 재사용한다(키 바이트는 MVCC-불변이다). btree_delete_overflow_key는 내장된 head VPID를 추출하고 체인을 해제하지만, 슬롯 자체는 건드리지 않는다(“btree 슬롯은 삭제하지 않는다”):
// btree_delete_overflow_key -- src/storage/btree.c (condensed)if (spage_get_record (thread_p, page_ptr, slot_id, &rec, PEEK) != S_SUCCESS) /* rec peek */ { goto exit_on_error; }if (node_type == BTREE_LEAF_NODE) { int mvccids_size = 0; /* +OR_MVCCID_SIZE per INSID/DELID flag */ if (btree_record_object_is_flagged (rec.data, BTREE_OID_HAS_MVCC_INSID)) mvccids_size += OR_MVCCID_SIZE; if (btree_record_object_is_flagged (rec.data, BTREE_OID_HAS_MVCC_DELID)) mvccids_size += OR_MVCCID_SIZE; if (btree_leaf_is_flaged (&rec, BTREE_LEAF_RECORD_CLASS_OID)) { start_ptr = rec.data + (2 * OR_OID_SIZE) + mvccids_size; } /* OID + class OID */ else { start_ptr = rec.data + OR_OID_SIZE + mvccids_size; } /* OID만 */ }else { start_ptr = rec.data + NON_LEAF_RECORD_SIZE; } /* 비-리프: 고정 prefix */or_init (&buf, start_ptr, DISK_VPID_SIZE);page_vpid.pageid = or_get_int (&buf, &rc);if (rc == NO_ERROR) { page_vpid.volid = or_get_short (&buf, &rc); }if (rc != NO_ERROR) { goto exit_on_error; }if (overflow_delete (thread_p, &(btid->ovfid), &page_vpid) == NULL) /* <- 체인 해제 */ { goto exit_on_error; }return NO_ERROR;리프/비-리프 분기(인라인 주석)는 가변 형태의 레코드 안에서 head VPID를 찾는 역할만 한다. 세 가지 실패 지점(spage_get_record, or_get_int/or_get_short, overflow_delete)은 모두 exit_on_error로 이동하며 rc(또는 ER_FAILED)를 반환한다. PAGE_OVERFLOW 페이지 위의 FILE_BTREE_OVERFLOW_KEY 체인으로서(VFID btid->ovfid), 파괴의 핵심 로직은 heap 경우(§6.1/§6.2)와 동일하다.
6.7 대조 — OID-list 반환은 이 경로를 사용하지 않는다
섹션 제목: “6.7 대조 — OID-list 반환은 이 경로를 사용하지 않는다”두 종류의 B+Tree overflow 형태는 btid->ovfid 파일을 공유하지만, 삭제 시점에서 갈린다. overflow-key 체인(이 챕터)은 overflow_delete로 일괄 해제된다. overflow-OID-list 체인(Chapter 7)은 overflow_delete로 해제되지 않는다 — PAGE_BTREE slotted 페이지 위에서 OID 단위로 회수(btree_overflow_remove_object)하며, 비워진 페이지는 개별적으로 언링크되고 file_dealloc된다. 키 체인 페이지만이 이 챕터의 경로를 흐른다.
6.8 챕터 요약 — 핵심 정리
섹션 제목: “6.8 챕터 요약 — 핵심 정리”- 반환은 얇은 순회다:
overflow_delete→overflow_traverse(OVERFLOW_DO_DELETE)→overflow_delete_internal(pgbuf_unfix_and_init후file_dealloc), 각 페이지를 해제하기 전에next_vpid를 수집한다. 모든 오류 분기는 NULL을 반환한다. - 삭제 시 페이지별
RVOVF_*레코드는 생성되지 않는다 — 복구는file_dealloc에 위임된다(RVFL_DEALLOCpostpone, numerable-file 분기에서는 추가로RVFL_USER_PAGE_MARK_DELETE). 따라서 순회 중 실패해도 롤백이 불필요하다. FILE_UNKNOWN_TYPE힌트는 정확성과 무관한 TODO다 —file_dealloc이 헤더에서 실제 타입을 읽도록 강제하며, 호출자는 타입을 알고 있으나 넘기지 않는다.REC_BIGONE에 대한 MVCC 삭제는 체인을 해제하지 않는다:is_mvcc_op분기는 head overflow 페이지에 DELID만 기록하고(heap_get_mvcc_rec_header_from_overflow→heap_delete_adjust_header→heap_set_mvcc_rec_header_on_overflow), home 슬롯에는 no-change 레코드를 남긴다 — 체인은 논리적 삭제보다 오래 살아있어야 한다.- overflow head의 MVCC 헤더는 항상
OR_MVCC_MAX_HEADER_SIZE로 강제된다 (누락된 INSID/DELID 패딩 포함). 덕분에 DELID를 인-플레이스로 기록해도 row 바디가 이동하지 않는다 — 핵심 정리 4의 전제 조건. - 체인은 vacuum 또는 비-MVCC 삭제에 의해서만 해제된다: 해당 분기는 home 슬롯을 먼저 회수하고(
heap_delete_physical→REC_MARKDELETED, 또는heap_is_reusable_oid시RVHF_MARK_REUSABLE_SLOTpostpone로REC_DELETED_WILL_REUSE) 그 다음heap_ovf_delete를 호출한다. - B+Tree overflow-key 삭제에는 MVCC 지연이 없다:
btree_delete_overflow_key는 head VPID를 추출하고 즉시overflow_delete를 호출하며, 슬롯 처리는 호출자에게 맡긴다 —overflow_delete를 전혀 사용하지 않는 OID-list 반환(Chapter 7)과는 대조적이다.
Chapter 7: B+Tree OID 리스트 변형 구현
섹션 제목: “Chapter 7: B+Tree OID 리스트 변형 구현”B+tree 인덱스는 overflow_file.c와 동일한 개념이 필요하다 — 하나의 논리적 레코드가 슬롯에 들어가기에 너무 크면 페이지 체인으로 흘러넘치게 된다 — 그러나 여기서는 하나의 인덱스 키를 공유하는 OID들의 정렬된 리스트를 위한 것이다. CUBRID는 overflow_file.c를 재사용하지 않는다; 대신 일반적인 PAGE_BTREE 슬롯 페이지 위에서 btree.c / btree_load.c 안에 체인을 독자적으로 구현한다. 디스크 상의 OID 인코딩은 cubrid-overflow-file.md의 “B+tree OID overflow” 절을 참조하고, 변형이 존재하는 이유는 §7.10에서 정리한다.
불변 조건 (페이지 타입 도메인). 모든 overflow OID 페이지는 pgbuf_check_page_ptype(...) == PAGE_BTREE를 만족하며, 이는 각 진입점에서 디버그 빌드 시 (#if !defined(NDEBUG) 내부에서) 단언된다. NDEBUG 빌드에서는 이 검사가 컴파일 아웃된다: 디버그에서는 잘못된 타입을 단언하지만, 릴리즈에서는 날(raw) 바이트를 슬롯 OID 레코드로 오독하여 리스트를 손상시킬 수 있다.
7.1 페이지 상의 레이아웃과 BTREE_OVERFLOW_HEADER
섹션 제목: “7.1 페이지 상의 레이아웃과 BTREE_OVERFLOW_HEADER”슬롯 0 / HEADER는 BTREE_OVERFLOW_HEADER이고, 슬롯 1은 하나의 OID 레코드로 BTREE_OBJECT_FIXED_SIZE 보폭(stride)에 따라 오름차순으로 정렬되어 있다.
graph LR leaf["leaf record<br/>OVERFLOW_OIDS<br/>link=first vpid"] o1["page A HEAD<br/>slot0 next=B"] o2["page B<br/>slot0 next=C"] o3["page C TAIL<br/>slot0 next=NULL"] leaf --> o1 --> o2 --> o3
Figure 7-1. 하나의 키에 대한 overflow 체인. leaf는 HEAD를 가리키며, 각 페이지의 slot-0 헤더가 단방향 연결 next_vpid를 보유한다.
// btree_overflow_header -- src/storage/btree_load.hstruct btree_overflow_header{ VPID next_vpid;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
next_vpid | 이 키의 체인에서 다음 페이지의 VPID; 꼬리(tail)에서는 VPID_ISNULL. | 체인은 HEAD→tail로 단방향 연결된다. leaf는 HEAD만 보관하고, 이후의 모든 hop에서 이 필드를 읽는다. 역방향 포인터도 없고, 길이 카운트도 없다 — 순회와 접합(splice)에 필요한 최솟값은 VPID 하나뿐이다. |
불변 조건 (슬롯 1에 단일 OID 레코드). 오브젝트 데이터는 슬롯 1에만 존재하고, 슬롯 0은 헤더다. 읽기 경로는 spage_get_record(..., 1, ...)와 HEADER를 하드코딩한다. 세 번째 슬롯이 생기면 이진 탐색의 stride 계산이 깨진다.
불변 조건 (고정 stride, 필수 전체 MVCC 헤더). 각 오브젝트는 BTREE_OBJECT_FIXED_SIZE(btid) 바이트이며 OID, 선택적 클래스 OID, 전체 OR_MVCC_MAX_HEADER_SIZE 헤더를 포함한다 (leaf 레코드는 이를 생략할 수 있다). 이진 탐색이 data + size * mid로 인덱싱하므로, 오브젝트가 짧으면 레코드 중간에 착지한다. 삽입 시 BTREE_MVCC_INFO_SET_FIXED_SIZE(...)로 stride를 강제한다.
7.2 btree_load.c의 헤더 접근자
섹션 제목: “7.2 btree_load.c의 헤더 접근자”슬롯 0에 대한 유일하게 허가된 접근자들이다.
// btree_get_overflow_header -- src/storage/btree_load.c#if !defined(NDEBUG) (void) pgbuf_check_page_ptype (thread_p, page_ptr, PAGE_BTREE); /* <- 디버그 전용 도메인 검사 */#endif if (spage_get_record (thread_p, page_ptr, HEADER, &header_record, PEEK) != S_SUCCESS) { assert_release (false); return NULL; } /* <- HEADER 슬롯이 반드시 존재해야 함 */ return (BTREE_OVERFLOW_HEADER *) header_record.data; /* <- PEEK: 페이지 버퍼를 별칭(alias) */PEEK은 페이지 버퍼를 별칭(alias)하므로, btree_get_next_overflow_vpid는 btree_get_overflow_header를 호출하여 (NULL 헤더면 ER_FAILED 반환) ovf_header->next_vpid를 값으로 호출자의 *vpid에 복사한다 — 덕분에 호출자는 이후 페이지를 안전하게 unfix할 수 있다.
btree_init_overflow_header는 슬롯 0을 설치하지만 자체적으로는 아무것도 로그하지 않는다 — 호출자가 첫 번째 오브젝트와 함께 하나의 redo 레코드로 묶는다. 유일한 실패 분기:
// btree_init_overflow_header -- src/storage/btree_load.c// ... condensed: memcpy ovf_header into rec, rec.length = sizeof(...), rec.type = REC_HOME ...if (spage_insert_at (thread_p, page_ptr, HEADER, &rec) != SP_SUCCESS) { return ER_FAILED; } /* <- 유일한 오류 경로 */7.3 체인 헤드 생성: btree_start_overflow_page
섹션 제목: “7.3 체인 헤드 생성: btree_start_overflow_page”새 페이지가 HEAD가 된다: next_vpid가 현재의 첫 번째 overflow VPID로 설정되고, 호출자(§7.4)가 leaf 링크를 다시 쓴다. 아래의 모든 분기가 인라인으로 주석 처리되어 있다 — btree_get_new_page NULL, init != NO_ERROR (unfix 없이 반환), spage_insert_at 실패, 그 다음 redo 전용 로그 + dirty:
// btree_start_overflow_page -- src/storage/btree.c*new_page_ptr = btree_get_new_page (thread_p, btid_int, new_vpid, near_vpid);if (*new_page_ptr == NULL) { ASSERT_ERROR_AND_SET (error_code); return error_code; }VPID_COPY (&ovf_header_info.next_vpid, first_overflow_vpid); /* <- HEAD는 기존 첫 번째 페이지를 가리킴 */error_code = btree_init_overflow_header (thread_p, *new_page_ptr, &ovf_header_info);// ... condensed: build rec, BTREE_OVERFLOW_NODE; init!=NO_ERROR returns WITHOUT unfixing ...if (spage_insert_at (thread_p, *new_page_ptr, 1, &rec) != SP_SUCCESS) /* <- slot 1 = 오브젝트들 */ { assert_release (false); return ER_FAILED; }// ... condensed: redo buffer = first_overflow_vpid + rec.data (link restored by redo too) ...log_append_redo_data (thread_p, RVBT_RECORD_MODIFY_NO_UNDO, &addr, rv_redo_data_length, rv_redo_data);pgbuf_set_dirty (thread_p, *new_page_ptr, DONT_FREE);Redo 전용: 롤백 시 주변 sysop이 할당 전체를 되돌리므로(§7.4), 물리적으로 따로 되돌릴 것이 없다.
7.4 헤드 연결과 페이지 누수 주석
섹션 제목: “7.4 헤드 연결과 페이지 누수 주석”호출자 btree_key_append_object_as_new_overflow는 하나의 시스템 연산(system operation) 하에서 leaf 링크를 다시 쓴다. 핵심 세 줄은 sysop 시작, 소스 자체의 누수 페이지 주석, leaf 재작성 로그 호출이다:
// btree_key_append_object_as_new_overflow -- src/storage/btree.cif (!insert_helper->is_system_op_started) { log_sysop_start (thread_p); /* ... set flag ... */ }/* Note that this page may be leaked if server crashes before changing the link in leaf page. */// ... condensed: btree_start_overflow_page, btree_leaf_record_change_overflow_link, spage_update leaf ...log_append_undoredo_data (thread_p, RVBT_RECORD_MODIFY_UNDOREDO, &insert_helper->leaf_addr, ...);불변 조건 (체인 도달 가능성 vs. 누수 페이지 윈도우). 페이지는 선행 페이지가 자신을 링크한 후에야 라이브 체인에 합류한다: HEAD의 next_vpid가 먼저 설정되고, leaf 링크가 두 번째로 설정되며, sysop이 할당과 leaf 링크 재작성을 하나의 원자 단위로 묶는다. 따라서 복구는 둘 다 갖거나 둘 다 갖지 않는다. 남는 유일한 누수는 소스 주석이 명시한 sysop-end 이전 크래시 경우 — 타입이 올바르고, 참조되지 않은 무해한 페이지다. error: 레이블은 sysop을 중단하고 ovfl_page를 unfix한다.
7.5 여유 공간 탐색: btree_find_free_overflow_oids_page
섹션 제목: “7.5 여유 공간 탐색: btree_find_free_overflow_oids_page”세 가지 분기가 있는 선형 WRITE-latch 최초 적합(first-fit) 스캔으로, 인라인 주석이 달려 있다:
// btree_find_free_overflow_oids_page -- src/storage/btree.cint space_needed = BTREE_OBJECT_FIXED_SIZE (btid);ovfl_vpid = *first_ovfl_vpid; /* 비어 있으면 루프 건너뜀, *overflow_page NULL */while (!VPID_ISNULL (&ovfl_vpid)) { *overflow_page = pgbuf_fix (...); if (*overflow_page == NULL) { ASSERT_ERROR_AND_SET (error_code); return error_code; } if (spage_max_space_for_new_record (thread_p, *overflow_page) > space_needed) { return NO_ERROR; } /* <- 적중: 페이지를 FIXED 상태로 호출자에게 반환 */ btree_get_next_overflow_vpid (thread_p, *overflow_page, &ovfl_vpid); pgbuf_unfix_and_init (thread_p, *overflow_page); /* <- 공간 없음: 다음 hop 전에 해제 */ }return NO_ERROR; /* <- 미적중: *overflow_page == NULL, 체인 전체 가득 참 */적중 시 페이지가 fixed 상태로 반환되고, 미적중 시 *overflow_page == NULL을 반환하여 호출자에게 새 HEAD를 시작하도록 알린다.
7.6 탐색: btree_find_oid_and_its_page와 btree_find_oid_from_ovfl
섹션 제목: “7.6 탐색: btree_find_oid_and_its_page와 btree_find_oid_from_ovfl”btree_find_oid_and_its_page는 먼저 leaf를 검사하고 (적중 시 *found_page = leaf), null 링크면 단락(short-circuit)한 뒤, 이전 페이지를 추적하면서 체인을 순회한다. 모든 분기가 아래에 인라인으로 주석 처리되어 있다; 루프 이후의 후처리 (적중 시 *found_page/*prev_page 할당, 미적중 시 unfix)는 생략되었다:
// btree_find_oid_and_its_page -- src/storage/btree.cif (*offset_to_object != NOT_FOUND) { *found_page = leaf_page; return NO_ERROR; } /* leaf 적중 */if (VPID_ISNULL (&leaf_rec_info->ovfl)) { return NO_ERROR; } /* <- 체인 없음 */do { overflow_page = pgbuf_fix (...); /* NULL -> goto error */ error_code = btree_find_oid_from_ovfl (...); /* err -> goto error */ if (*offset_to_object != NOT_FOUND) { break; } /* <- 적중: 페이지 fixed 유지 */ if (prev_overflow_page != NULL) { pgbuf_unfix_and_init (thread_p, prev_overflow_page); } prev_overflow_page = overflow_page; overflow_page = NULL; /* <- prev 포인터 전진 */ error_code = btree_get_next_overflow_vpid (thread_p, prev_overflow_page, &overflow_vpid); if (error_code != NO_ERROR) { ASSERT_ERROR (); goto error; }} while (!VPID_ISNULL (&overflow_vpid));// ... condensed: assign *found_page / *prev_page, or unfix on not-found ...불변 조건 (prev_page 계약). 체인 적중 시 prev_page가 요청되면, 함수는 바로 앞 페이지를 반환한다 — 또 다른 overflow 페이지이거나, 첫 번째 overflow 페이지에서 적중했을 때는 leaf — 이는 unlink 경로(§7.8)가 필요로 하는 정확한 값이다. thread_p->read_ovfl_pages_count는 vacuum을 위해 증가된다.
btree_find_oid_from_ovfl은 정렬된 슬롯 1에 대한 페이지 단위 이진 탐색이다: 슬롯 1을 PEEK (실패 시 assert_release); OID_LT/OID_GT를 이용한 첫/마지막 오브젝트 조기 탈출; 그 다음 [1 .. num_oids-2] 구간에서 while (min <= max) 탐색. 두 가지 핵심 세부사항은 stride 계산과 OID 일치 시의 위임이다:
// btree_find_oid_from_ovfl -- src/storage/btree.csize = BTREE_OBJECT_FIXED_SIZE (btid_int); /* <- 고정 stride가 탐색 불변 조건 */oid_ptr = ovf_record.data + (size * mid); /* <- stride 계산은 고정 크기에 의존 */if (OID_EQ (oid, &inst_oid)) /* <- OID 일치: MVCC 런(run) 스캔, 아니면 +/- mid */ { return btree_seq_find_oid_from_ovfl (thread_p, btid_int, oid, &ovf_record, oid_ptr, ...); }// ... else OID_GT -> min = mid+1; OID_LT -> max = mid-1; fall through -> NO_ERROR (not found) ...7.7 링크 재작성: btree_modify_overflow_link
섹션 제목: “7.7 링크 재작성: btree_modify_overflow_link”제거된 페이지의 선행자가 또 다른 overflow 페이지일 때, 그 next_vpid를 재작성한다 — 라이브 overflow 헤더가 변경되는 유일한 지점이다. 생존하는 페이지에 대한 제자리 변경이므로 RVBT_RECORD_MODIFY_UNDOREDO를 사용한다.
// btree_modify_overflow_link -- src/storage/btree.cassert (delete_helper->is_system_op_started); /* <- 호출자가 sysop을 소유해야 함 */overflow_header_record.data = rv_undo_data;if (spage_get_record (thread_p, ovfl_page, HEADER, &overflow_header_record, COPY) != S_SUCCESS) { assert_release (false); return ER_FAILED; } /* <- 오류 분기 1: undo 이미지 읽기 */VPID_COPY (&ovf_header_info.next_vpid, next_ovfl_vpid); /* <- 새 링크가 제거된 페이지를 건너뜀 */overflow_header_record.data = (char *) &ovf_header_info; // ... condensed: length set ...if (spage_update (thread_p, ovfl_page, HEADER, &overflow_header_record) != SP_SUCCESS) { assert_release (false); return ER_FAILED; } /* <- 오류 분기 2: update */ovf_addr.offset = HEADER; /* <- 슬롯 0, 슬롯 1 아님: 헤더 재작성 */BTREE_RV_SET_OVERFLOW_NODE (&ovf_addr);LOG_RV_RECORD_SET_MODIFY_MODE (&ovf_addr, LOG_RV_RECORD_UPDATE_ALL); /* <- 전체 헤더 교체 */log_append_undoredo_data (thread_p, RVBT_RECORD_MODIFY_UNDOREDO, &ovf_addr, ...);Undo = 기존 헤더 (먼저 복사됨), redo = 새 헤더, 1-필드 레코드 전체(LOG_RV_RECORD_UPDATE_ALL), 슬롯 HEADER를 주소로 지정. 두 가지 오류 분기: undo 이미지 읽기(spage_get_record COPY)와 spage_update — 둘 다 assert_release(false) 후 ER_FAILED 반환. leaf 선행자 변형은 btree_modify_leaf_ovfl_vpid이며, 마찬가지로 RVBT_RECORD_MODIFY_UNDOREDO를 사용한다.
7.8 축소: btree_overflow_remove_object
섹션 제목: “7.8 축소: btree_overflow_remove_object”페이지가 비게 되는지 여부에 따른 두 가지 최상위 분기.
flowchart TD
R["spage_get_record slot1 COPY"] --> Q{length == FIXED_SIZE?}
Q -->|yes: 마지막 오브젝트| GN{get_next 오류?}
GN -->|yes| RET["return err; 아직 sysop 없음"]
GN -->|no| U["unfix; sysop_start"]
U --> DA["file_dealloc page"]
DA --> DAE{dealloc 오류?}
DAE -->|yes| ERR["goto error: 시작했으면 sysop_end"]
DAE -->|no| LK{prev_page == leaf?}
LK -->|yes| LM["btree_modify_leaf_ovfl_vpid -> next"]
LK -->|no| OM["btree_modify_overflow_link -> next"]
LM --> SE["시작했으면 btree_delete_sysop_end"]
OM --> SE
Q -->|no: 오브젝트 더 있음| RR["btree_record_remove_object slot1"]
RR --> OK["NO_ERROR"]
SE --> OK
Figure 7-2. btree_overflow_remove_object 분기 지도. get_next_overflow_vpid 실패 후의 조기 bare return을 주목하라: 이는 sysop이 열리기 전에 발생하므로, 이후의 dealloc/link 실패가 사용하는 error: 레이블을 거치지 않는다.
빈 분기의 핵심 줄은 조기 bare return과 dealloc 이전 unfix 순서이다:
// btree_overflow_remove_object -- src/storage/btree.cif (overflow_record.length == BTREE_OBJECT_FIXED_SIZE (btid_int)) { assert (offset_to_object == 0); /* <- 단일 오브젝트는 오프셋 0에 있음 */ error_code = btree_get_next_overflow_vpid (thread_p, *overflow_page, &next_overflow_vpid); if (error_code != NO_ERROR) { ASSERT_ERROR (); return error_code; } /* <- bare return: 아직 sysop 없음 */ pgbuf_get_vpid (*overflow_page, &overflow_vpid); pgbuf_unfix_and_init (thread_p, *overflow_page); /* <- dealloc 이전에 unfix */ // ... condensed: sysop_start if needed; file_dealloc (goto error on fail) ... if (prev_page == leaf_page) /* <- §7.7: leaf vs overflow 선행자 */ { /* btree_modify_leaf_ovfl_vpid */ } else { /* btree_modify_overflow_link */ } // ... condensed: goto error on fail; btree_delete_sysop_end if !save_system_op_started ... }else // ... condensed: 비어 있지 않은 페이지는 slot 1에서 btree_record_remove_object로 오브젝트 하나를 접합 제거 ...불변 조건 (sysop 하에서 해제, 먼저 unfix). 해제는 sysop 안에서 이루어져야 한다. 그렇지 않으면 롤백 시 페이지가 누수된다 (소스가 명시); 코드는 필요하면 sysop을 시작하고, file_dealloc 이전에 unfix하며, 동일한 sysop 안에서 unlink를 실행한다 — “오브젝트 제거 + 페이지 해제 + 선행자 재연결”을 원자적으로 만든다. error: 레이블은 이 함수가 sysop을 열었을 때만 (!save_system_op_started) 종료시켜 이중 종료를 방지한다; 조기 get_next 실패는 sysop이 존재하기 전에 반환하므로 error:를 거쳐서는 안 된다.
7.9 제자리 교체: btree_overflow_record_replace_object
섹션 제목: “7.9 제자리 교체: btree_overflow_record_replace_object”고유 인덱스 “첫 번째 leaf 오브젝트를 마지막 overflow 오브젝트와 교환” 삭제 패턴의 overflow 측 교체 — 제자리에서, 호출자의 sysop 하에서, 단일 분기:
// btree_overflow_record_replace_object -- src/storage/btree.cassert (delete_helper->is_system_op_started); /* <- 반드시 sysop 내부에서 실행해야 함 */// ... condensed: overflow_addr -> slot 1; BTREE_RV_SET_OVERFLOW_NODE; btree_record_replace_object ...if (spage_update (thread_p, overflow_page, 1, overflow_record) != SP_SUCCESS) { assert_release (false); return ER_FAILED; } /* <- 유일한 오류 분기 */log_append_undoredo_data (thread_p, RVBT_RECORD_MODIFY_UNDOREDO, &overflow_addr, ...);두 오브젝트 모두 고정 크기이므로 길이가 변하지 않고, 정렬 순서는 구조적으로 보존된다 (교환은 마지막 오브젝트의 슬롯을 상속한다). FI_TEST 포인트가 절반만 적용된 복구 테스트를 위해 update 전후를 감싼다.
7.10 RVBT_*를 쓰고 RVOVF_*를 쓰지 않는 이유
섹션 제목: “7.10 RVBT_*를 쓰고 RVOVF_*를 쓰지 않는 이유”세 가지 속성이 이 변형을 강제한다: (1) 페이지가 PAGE_BTREE이고 FILE_OVERFLOW가 아닌 인덱스 자체 파일(btid_int->sys_btid->vfid)에서 할당된다; (2) 두 슬롯짜리 spage가 제자리 spage_update를 허용하는데, 연속적인 OVERFLOW_REST_PART 바이트 형식에서는 불가능하다; (3) 모든 변경은 RVBT_RECORD_MODIFY_NO_UNDO, ..._UNDOREDO, 또는 RVBT_GET_NEWPAGE를 로그하며 — 이 레코드들은 BTREE_RV_OVERFLOW_FLAG가 태그된 addr.offset을 가지고, 고정 크기 OID 인코딩과 B+tree 논리적 undo를 이해하는 B+tree 레코드 변경 적용 루틴이 재생한다. RVOVF_*는 FILE_OVERFLOW 파일의 불투명한 바이트만 이해하므로, 여기서 재사용하면 구조적으로 잘못된 페이지를 재생하게 된다 — 그리고 overflow 편집과 leaf-link 편집은 하나의 RVBT_* 계열 아래서 함께 재생된다.
7.11 챕터 요약 — 핵심 정리
섹션 제목: “7.11 챕터 요약 — 핵심 정리”- OID 리스트 체인은 인덱스 자체 파일의
PAGE_BTREE슬롯 페이지 위에서 의도적으로 재구현된 것이며,overflow_file.c를 재사용하지 않는다. BTREE_OVERFLOW_HEADER는 단일next_vpid이고, 체인은 HEAD→tail로 단방향 연결되며 leaf는 HEAD 링크만 보관한다.- 새 페이지는 하나의 sysop 하에서 HEAD 앞에 삽입된다 — 페이지
next_vpid먼저, leaf 링크 두 번째 — sysop-end 이전 크래시 시 무해한 누수가 문서화되어 있다. - 여유 공간 탐색은 최초 적합(first-fit)이고, 오브젝트 탐색은 고정 stride 이진 탐색이다.
btree_find_oid_and_its_page는 선행자 페이지를 반환하여, 삭제 시btree_modify_leaf_ovfl_vpid와btree_modify_overflow_link중 선택할 수 있게 한다.- 축소에는 두 분기가 있다: 마지막 오브젝트일 때 sysop 하에서 dealloc-and-unlink, 또는 생존 레코드에서
btree_record_remove_object를 통한 접합 제거; 조기get_next실패는 sysop이 열리기 전에 반환한다. - 모든 편집은
BTREE_RV_OVERFLOW_FLAG가 태그된 주소로RVBT_*를 로그한다 — 절대RVOVF_*가 아니다.
Chapter 8: 크래시 복구와 예외 경로
섹션 제목: “Chapter 8: 크래시 복구와 예외 경로”라이프사이클 챕터(3, 5, 6)는 뮤테이터가 무엇을 기록하는지를 다뤘다. 이 챕터는 다음 질문에 답한다: 서버가 두 로그 어펜드 사이에 죽는다면, 어느 레코드가 체인을 일관된 상태로 재현하는가, 그리고 어느 경로는 정상 라이프사이클을 전혀 거치지 않는가?
네 개의 recovery index가 이 서브시스템 전체를 담당한다(핸들러 네 개, 덤프 헬퍼 두 개). 다섯 번째 레코드인 길이 0의 LOG_DUMMY_OVF_RECORD는 복구 동작 없이, HA 리플리케이션과 vacuum 헤드 식별의 앵커 역할만 한다. undo, redo, physical logging, sysop 브래킷에 대한 개요는 상위 컴패니언 문서 cubrid-overflow-file.md를 참고한다.
8.1 디스패치 테이블: 네 개의 인덱스, 네 개의 핸들러
섹션 제목: “8.1 디스패치 테이블: 네 개의 인덱스, 네 개의 핸들러”복구는 테이블 구동 방식이다. 각 rcvindex는 recovery.c 내 RV_fun[]의 행 하나에 대응하며, 구조는 { rcvindex, name, undofun, redofun, undo_dump, redo_dump }이다:
// RV_fun[] -- src/transaction/recovery.c {RVOVF_NEWPAGE_INSERT, "RVOVF_NEWPAGE_INSERT", NULL, /* <- no undo: insert is redo-only */ overflow_rv_newpage_insert_redo, NULL, log_rv_dump_hexa}, {RVOVF_NEWPAGE_LINK, "RVOVF_NEWPAGE_LINK", overflow_rv_newpage_link_undo, /* <- undo: cut the new link to NULL */ overflow_rv_link, /* <- redo: re-establish the link */ overflow_rv_link_dump, overflow_rv_link_dump}, {RVOVF_PAGE_UPDATE, "RVOVF_PAGE_UPDATE", log_rv_copy_char, /* <- undo: restore the before-image */ overflow_rv_page_update_redo, /* <- redo: set ptype then after-image */ overflow_rv_page_dump, overflow_rv_page_dump}, {RVOVF_CHANGE_LINK, "RVOVF_CHANGE_LINK", overflow_rv_link, overflow_rv_link, /* <- undo and redo are the SAME fn */ overflow_rv_link_dump, overflow_rv_link_dump},테이블에서 두 가지 설계 사실이 바로 눈에 띈다:
RVOVF_NEWPAGE_INSERT의 undo 슬롯은NULL이다. 삽입은 페이지 쓰기를 되돌리지 않는다. abort 시에는 sysop이 페이지를 해제하므로 un-write할 내용 자체가 없다.RVOVF_CHANGE_LINK는 undo와 redo에 동일한 함수를 사용한다. 레코드 자체가 대칭적(undo용 old VPID, redo용 new VPID)이며, 핸들러는 복구 시스템이 건네주는 값을next_vpid에 복사할 뿐이다.
flowchart TD
subgraph idx["recovery 인덱스 (recovery.h 100..103, 값 54..57)"]
A["RVOVF_NEWPAGE_INSERT"]
B["RVOVF_NEWPAGE_LINK"]
C["RVOVF_PAGE_UPDATE"]
D["RVOVF_CHANGE_LINK"]
end
A -->|redo| H1["overflow_rv_newpage_insert_redo<br/>= log_rv_copy_char"]
B -->|undo| H2["overflow_rv_newpage_link_undo<br/>next_vpid := NULL"]
B -->|redo| H3["overflow_rv_link<br/>next_vpid := data"]
C -->|undo| H4["log_rv_copy_char<br/>before-image"]
C -->|redo| H5["overflow_rv_page_update_redo<br/>ptype 재설정 + after-image"]
D -->|undo / redo| H3
Figure 8-1. 네 인덱스와 각 핸들러. overflow_rv_link는 네 슬롯 중 세 개에서 공유된다.
8.2 RVOVF_NEWPAGE_INSERT — redo 전용 전체 페이지 재현
섹션 제목: “8.2 RVOVF_NEWPAGE_INSERT — redo 전용 전체 페이지 재현”핸들러는 제네릭 log_rv_copy_char 위에 한 줄짜리 래퍼다. log_rv_copy_char는 memcpy(rcv->pgptr + rcv->offset, rcv->data, rcv->length) 후 pgbuf_set_dirty를 수행한다. 여기서 rcv->offset은 항상 0이므로 레코드는 페이지 맨 앞에 기록된다:
// overflow_rv_newpage_insert_redo -- src/storage/overflow_file.cint overflow_rv_newpage_insert_redo (THREAD_ENTRY * thread_p, LOG_RCV * rcv){ return log_rv_copy_char (thread_p, rcv); }발행 지점 (챕터 3). overflow_insert는 next_vpid, length, 페이로드를 스탬프한 뒤 페이지당 한 번씩 이 레코드를 로깅한다. log_append_redo_data(…, RVOVF_NEWPAGE_INSERT, &addr, copy_length + (copyto - addr.pgptr), addr.pgptr) — 소스 포인터는 copyto가 아니라 페이지 맨 앞(addr.pgptr)이다. 따라서 redo 이미지는 이미 설정된 next_vpid를 포함한 헤더 플러스 페이로드이며, 레코드 하나로 단일 memcpy에 앞부분 전체를 재구성한다. redo 전용인 이유: 콘텐츠와 링크가 하나의 원자적 레코드를 공유하므로 절반만 쓰여진 페이지를 undo할 상황이 없다. abort 시에는 sysop(§8.8)이 페이지를 회수한다.
8.3 RVOVF_NEWPAGE_LINK — undo는 링크를 끊고, redo는 링크를 복원한다
섹션 제목: “8.3 RVOVF_NEWPAGE_LINK — undo는 링크를 끊고, redo는 링크를 복원한다”이 인덱스는 overflow_update(챕터 5)에서만 나타난다. 재작성 중 페이지가 부족해 체인을 연장해야 할 때 사용된다. file_alloc으로 새 next_vpid를 확보한 뒤 log_append_undoredo_data(…, RVOVF_NEWPAGE_LINK, &addr, 0, sizeof(VPID), NULL, &next_vpid)를 로깅한다. 레코드는 비대칭이다 — undo 길이 0, redo 길이 sizeof(VPID) — 이전에 존재하지 않던 forward pointer를 새로 만들었다는 시그니처다.
// overflow_rv_link -- src/storage/overflow_file.c (redo: install the pointer){ ((OVERFLOW_REST_PART *) rcv->pgptr)->next_vpid = *(VPID *) rcv->data; /* <- splice in */ pgbuf_set_dirty (thread_p, rcv->pgptr, DONT_FREE); ... }
// overflow_rv_newpage_link_undo -- src/storage/overflow_file.c (undo: sever){ VPID_SET_NULL (&((OVERFLOW_REST_PART *) rcv->pgptr)->next_vpid); /* <- ignores rcv->data */ pgbuf_set_dirty (thread_p, rcv->pgptr, DONT_FREE); ... }undo 핸들러는 rcv->data를 무시한다(따라서 undo 길이 0). undo는 항상 “이전에 forward link가 없었다”를 의미하기 때문이다. 두 핸들러 모두 첫 번째 페이지에도 OVERFLOW_REST_PART로 캐스팅한다. next_vpid가 두 구조체 모두에서 offset 0에 위치하기 때문이다(챕터 1).
불변 조건 — 도달 가능한 페이지는 절대 댕글링 소스가 아니다. 선행 페이지의
next_vpid는 완전히 복구 가능한 후속 페이지를 가리키거나 NULL이다. 회수된 페이지를 가리키는 경우는 없다. 순서 보장으로 강제된다: 페이지 할당(별도 로그)이RVOVF_NEWPAGE_LINK보다 먼저 이루어지며, 둘 다 동일한 sysop 안에 있다. undo 시 링크는 NULL이 되고 파일 undo가 후속 페이지를 회수한다 — 어느 재현 순서에서도 선행 페이지가 회수된 페이지를 가리키는 일은 없다. 이 조건이 깨지면overflow_traverse(챕터 4)가 해제된 페이지로 걸어들어가 쓰레기 값을next_vpid로 읽게 된다.
8.4 RVOVF_CHANGE_LINK — 축소를 위한 대칭 링크 재작성
섹션 제목: “8.4 RVOVF_CHANGE_LINK — 축소를 위한 대칭 링크 재작성”overflow_update가 여분의 페이지를 남기며 복사를 마치면, 마지막으로 필요한 페이지의 링크를 NULL로 만들고 꼬리를 해제해 체인을 축소한다(챕터 5, 6). tmp_vpid를 NULL로 설정한 뒤 log_append_undoredo_data(…, RVOVF_CHANGE_LINK, &addr, sizeof(VPID), sizeof(VPID), &next_vpid, &tmp_vpid)를 로깅한다 — 양쪽 이미지가 모두 완전한 VPID다: undo용으로는 사라질 꼬리를 가리키던 old forward link, redo용으로는 새 값(NULL). 연산의 본질이 “VPID 하나를 다른 값으로 교체”이므로 undo와 redo가 overflow_rv_link를 공유한다 — undo는 old VPID를 복원하고, redo는 NULL을 기록한다(따라서 §8.1에서 양쪽 슬롯에 모두 등장한다).
stateDiagram-v2 direction LR [*] --> Linked: 페이지가 꼬리를 가리킴 Linked --> Unlinked: redo CHANGE_LINK \n next_vpid를 NULL로 설정 Unlinked --> Linked: undo CHANGE_LINK \n next_vpid를 data에서 복원
Figure 8-2. RVOVF_CHANGE_LINK는 제자리에서 가역적이다. 레코드가 양 끝점을 모두 담고 있으므로 어느 방향이든 next_vpid에 복사하는 것으로 처리된다.
NEWPAGE_LINK와의 차이. 둘 다 next_vpid를 재작성하지만 서로 교환 불가능하다: NEWPAGE_LINK는 체인을 확장하고 undo 시 NULL로 되돌린다(레코드에 없음); CHANGE_LINK는 체인을 축소하고 undo 시 old 값으로 복원한다(레코드에 있음). 두 인덱스가 겹치는 지점은 redo뿐이다.
8.5 RVOVF_PAGE_UPDATE — ptype 가드를 포함한 before/after 이미지
섹션 제목: “8.5 RVOVF_PAGE_UPDATE — ptype 가드를 포함한 before/after 이미지”인플레이스 콘텐츠 재작성(챕터 5)은 덮어쓰는 페이지마다 undo용 before-image와 redo용 after-image를 로깅한다:
// overflow_update -- src/storage/overflow_file.c (first-part branch)if (hdr_length + old_length > DB_PAGESIZE) /* <- old payload spans full page */ log_append_undo_data (..., RVOVF_PAGE_UPDATE, &addr, DB_PAGESIZE, addr.pgptr);else /* <- only the used prefix */ log_append_undo_data (..., RVOVF_PAGE_UPDATE, &addr, hdr_length + old_length, addr.pgptr);// ... memcpy of new content, then ...log_append_redo_data (..., RVOVF_PAGE_UPDATE, &addr, copy_length + hdr_length, addr.pgptr);undo 길이는 old 페이로드가 페이지를 가득 채웠을 때 DB_PAGESIZE, 그렇지 않으면 사용된 접두부만 — 뒤쪽 쓰레기 바이트는 절대 포함하지 않는다. redo 길이 copy_length + hdr_length는 헤더와 새 슬랩의 합이다.
// overflow_rv_page_update_redo -- src/storage/overflow_file.c{ (void) pgbuf_set_page_ptype (thread_p, rcv->pgptr, PAGE_OVERFLOW); /* <- re-stamp */ return log_rv_copy_char (thread_p, rcv); } /* <- then bytes */undo 핸들러는 순수한 log_rv_copy_char(§8.1 undo 슬롯)다. before-image는 ptype 수정이 필요 없다. 이미지를 찍었을 때 페이지가 이미 해당 타입을 갖고 있었기 때문이다.
redo에서
PAGE_OVERFLOW를 재스탬프하는 이유. 복구 시 after-image가 적용되는 페이지는 재할당 과정에서pgbuf의 페이지 타입이 초기화됐거나, 애초에 영속화된 적이 없을 수 있다.pgbuf_set_page_ptype호출은 redo를 그 자체로 완결 가능하게 만든다.
8.6 덤프 헬퍼
섹션 제목: “8.6 덤프 헬퍼”덤프 헬퍼는 로그 덤프 시 복구 페이로드를 텍스트로 렌더링한다. 상태를 변경하지 않는다.
// overflow_rv_page_dump -- src/storage/overflow_file.cvoid overflow_rv_page_dump (FILE * fp, int length, void *data){ OVERFLOW_REST_PART *rest_parts = (OVERFLOW_REST_PART *) data; int hdr_length = offsetof (OVERFLOW_REST_PART, data); /* <- skip link header */ fprintf (fp, "Overflow Link to Volid = %d|Pageid = %d\n", rest_parts->next_vpid.volid, rest_parts->next_vpid.pageid); log_rv_dump_char (fp, length - hdr_length, (char *) data + hdr_length); } /* <- ascii payload */overflow_rv_link_dump(fp, length_ignore, data)는 length를 무시하고(링크 레코드는 항상 VPID 하나) VPID를 fprintf한다. 두 링크 인덱스의 undo/redo 모두에 사용된다. overflow_rv_page_dump는 헤더 next_vpid를 출력한 뒤 나머지 length - hdr_length 바이트를 아스키 덤프한다 — RVOVF_PAGE_UPDATE redo 레이아웃에 대응한다. RVOVF_NEWPAGE_INSERT는 제네릭 log_rv_dump_hexa(§8.1)를 사용한다. 이미지가 페이지 맨 앞에서 시작하므로 16진수 덤프로 처리한다.
8.7 LOG_DUMMY_OVF_RECORD — 복구 동작 없는 길이 0의 앵커
섹션 제목: “8.7 LOG_DUMMY_OVF_RECORD — 복구 동작 없는 길이 0의 앵커”LOG_DUMMY_OVF_RECORD(“overflow 레코드 첫 번째 파트의 지시자”)는 RV_fun[]에 없다 — 핸들러도, undo/redo도 없다. 두 뮤테이터 모두 log_append_empty_record(길이 0 본문)로 첫 번째 페이지에서만 발행한다: overflow_insert의 i == 0 브랜치는 file_type != FILE_TEMP 조건으로 가드되고, overflow_update는 first-part 브랜치에서 발행한다.
이 레코드는 페이지별 레코드가 갖지 못한 인식 가능한 LSN 랜드마크다. 모든 페이지는 거의 동일한 RVOVF_NEWPAGE_INSERT / RVOVF_PAGE_UPDATE를 발행하므로, 로그 컨슈머는 하나의 overflow 객체가 어디서 끝나고 다음이 어디서 시작하는지 알 수 없다. 첫 번째 페이지에서만 발행되므로 객체의 head VPID를 표시한다 — vacuum이나 HA/CDC 컨슈머가 필요로 하는 앵커다(CDC 워커 cdc_get_overflow_recdes가 이 구분자의 사용처 중 하나다).
8.8 sysop 브래킷: 반쯤 만들어지거나 고아가 된 체인이 살아남지 못하는 이유
섹션 제목: “8.8 sysop 브래킷: 반쯤 만들어지거나 고아가 된 체인이 살아남지 못하는 이유”모든 뮤테이터는 할당과 쓰기를 sysop으로 감싼다: log_sysop_start, file_alloc_multiple(성공 시 file_init_page_type, FILE_TEMP의 경우 file_init_temp_page_type 전달), 페이지별 memcpy + RVOVF_NEWPAGE_INSERT, 그리고 성공 시 log_sysop_attach_to_outer — 오류 시에는 exit_on_error에서 log_sysop_abort. 이 브래킷은 개별 레코드로는 불가능한 전부-또는-전무 보장을 제공한다:
- attach 이전 → undo가
log_sysop_abort의 보상을 실행해 모든 페이지를 해제한다.RVOVF_NEWPAGE_INSERTredo 이미지는 살아남지 못한다. - attach 이후, 부모 커밋 이전 → 쓰기는 부모 트랜잭션에 귀속된다. abort 시 파일 레이어 undo가 페이지를 회수한다.
- 부모 커밋 이후 → redo가 각 자기 완결적 레코드를 순서대로 재현한다. 이미 플러시된 페이지가 어느 것이든 체인은 올바르게 조립된다.
overflow_update는 RVOVF_NEWPAGE_LINK / RVOVF_CHANGE_LINK를 같은 브래킷에 추가한다. 해당 undo 핸들러(§8.3, §8.4)는 선행 페이지의 next_vpid를 복원하므로, 부분적으로 크기가 조정된 체인은 업데이트 이전 토폴로지로 롤백된다 — 물리적 이미지와 sysop 해제가 모든 크래시 윈도우를 닫는다.
8.9 예외 경로: 임시 파일, no_logging, MVCC 피크, 디버그 덤퍼
섹션 제목: “8.9 예외 경로: 임시 파일, no_logging, MVCC 피크, 디버그 덤퍼”FILE_TEMP와 no_logging은 RVOVF 로깅을 억제한다. 임시 overflow 파일은 휘발성이므로 발행 지점이 가드된다: overflow_insert는 RVOVF_NEWPAGE_INSERT 어펜드를 if (file_type != FILE_TEMP && thread_p->no_logging != true) 조건으로 감싸고, overflow_update는 콘텐츠 redo를 no_logging != true만으로 가드한다. 로깅이 억제되면 해당 페이지에 대한 핸들러는 절대 호출되지 않는다 — 재현할 레코드 자체가 없다.
임시 페이지는 redo가 아닌 할당 시점에 타입을 받는다. redo 경로가 없으므로 타입은 처음부터 올바르게 설정되어야 한다: overflow_insert의 file_alloc_multiple은 FILE_TEMP일 때 file_init_page_type 대신 file_init_temp_page_type을 받는다(file_type != FILE_TEMP ? file_init_page_type : file_init_temp_page_type, §8.8). 재스탬프하는 레코드가 없기 때문이다. 이 분기는 오직 여기서만 존재한다: overflow_update는 assert (file_type == FILE_MULTIPAGE_OBJECT_HEAP)으로 시작하며 FILE_TEMP에 대해서는 절대 실행되지 않으므로, 확장 브랜치의 file_alloc은 임시 대안 없이 file_init_page_type에 하드와이어드다.
overflow_get_first_page_data를 이용한 MVCC 피크. 챕터 4의 순회 없이 인라인 페이로드만 필요한 독자는, 첫 번째 페이지를 fixed 상태로 보유하는 동안 이 접근자를 호출한다: assert (page_ptr != NULL) 후 ((OVERFLOW_FIRST_PART *) page_ptr)->data, 즉 link+length 헤더 너머의 주소를 반환한다. MVCC 코드는 이 접근자로 선두 바이트(MVCC id를 담은 레코드 헤더)를 인플레이스로 피크할 수 있다 — NULL이 아니라는 assert 이상의 fix, latch, 또는 검증 없이.
overflow_dump는 CUBRID_DEBUG 전용이다. exit에 영향을 주는 브랜치는 세 개다: pgbuf_fix 실패 조기 반환 두 개, 그리고 VPID_ISNULL 손상 검사 하나. head VPID에서의 초기 fix와 페이지 전진 후의 per-page fix는 모두 return ((ret = er_errid ()) == NO_ERROR) ? ER_FAILED : ret;로 bail out한다 — fix 실패는 NULL 페이지를 걷는 대신 덤프를 중단한다(페이지 버퍼 오류 또는 ER_FAILED 전파). 비직관적인 브랜치는 현재 페이지가 unfix된 뒤, remain_length > 0 루프 내부의 VPID_ISNULL 검사다:
// overflow_dump -- src/storage/overflow_file.c (#if defined (CUBRID_DEBUG)) while (remain_length > 0) { remain_length -= dump_length; if (remain_length > 0) { pgbuf_unfix_and_init (thread_p, pgptr); /* <- release current before checking */ if (VPID_ISNULL (&next_vpid)) /* <- chain ends early but bytes remain */ { er_set (..., ER_HEAP_OVFADDRESS_CORRUPTED, ...); return ...; } pgptr = pgbuf_fix (..., &next_vpid, ...); /* <- advance to next page */ if (pgptr == NULL) /* <- second fix-failure early return */ { return ((ret = er_errid ()) == NO_ERROR) ? ER_FAILED : ret; } next_vpid = ((OVERFLOW_REST_PART *) pgptr)->next_vpid; } }이 코드는 선언된 length가 링크가 공급하는 바이트보다 더 많이 요구하는 손상 케이스를 포착한다. NULL next pointer를 역참조하는 대신 오류를 올린다 — 챕터 4가 의존하는 length-대-링크 일관성과 동일한 불변 조건이다.
8.10 챕터 요약 — 핵심 정리
섹션 제목: “8.10 챕터 요약 — 핵심 정리”- 네 인덱스, 네 핸들러: insert(redo 전용 전체 페이지 복사), NEWPAGE_LINK(undo는 NULL / redo는 링크 설치), PAGE_UPDATE(undo는 before-image / redo는 ptype 재스탬프 후 after-image), CHANGE_LINK(대칭 VPID 스왑, 양방향 동일 핸들러).
overflow_rv_link는RVOVF_NEWPAGE_LINKredo와RVOVF_CHANGE_LINK양쪽 슬롯 모두에서 공유된다. 확장/축소의 비대칭성은 undo 쪽에만 존재한다(overflow_rv_newpage_link_undo는rcv->data를 무시하고 강제로 NULL). PAGE_UPDATE redo는PAGE_OVERFLOW를 재확인하는 유일한 경로다. insert와FILE_TEMP페이지는 할당 시file_init_page_type/file_init_temp_page_type에서 타입을 받는다. 이 temp/non-temp 분기는overflow_insert에만 존재한다.overflow_update는FILE_MULTIPAGE_OBJECT_HEAP을 assert하며 임시 파일을 절대 처리하지 않는다.- 크래시 후 반쯤 만들어지거나 고아가 된 체인은 살아남지 못한다: 콘텐츠와 링크는 레코드 단위로 원자적이고, 연산 전체는 abort 시 페이지를 해제하는 sysop으로 감싸져 있다.
FILE_TEMP/no_logging은 RVOVF 어펜드 자체를 건너뛴다. LOG_DUMMY_OVF_RECORD는 객체 헤드의 길이 0 LSN 랜드마크다.overflow_get_first_page_data는 latch 없는 MVCC 피크다.overflow_dump(CUBRID_DEBUG 전용)는pgbuf_fix실패 시 중단하고length-대-링크 불일치를 손상으로 플래그한다.
이 리비전 시점의 위치 힌트
섹션 제목: “이 리비전 시점의 위치 힌트”아래 줄 번호는 2026-06-23 시점에 관찰한 값이다. 심볼 이름이 정식 앵커이며 줄 번호는 시간이 지나면 어긋나는 힌트다.
| Symbol | File | Line |
|---|---|---|
CEIL_PTVDIV | src/base/memory_alloc.h | 57 |
BTREE_LEAF_RECORD_OVERFLOW_OIDS | src/storage/btree.c | 115 |
BTREE_LEAF_RECORD_OVERFLOW_KEY | src/storage/btree.c | 117 |
BTREE_RV_SET_OVERFLOW_NODE | src/storage/btree.c | 971 |
btree_create_overflow_key_file | src/storage/btree.c | 1975 |
btree_store_overflow_key | src/storage/btree.c | 2021 |
btree_load_overflow_key | src/storage/btree.c | 2131 |
btree_delete_overflow_key | src/storage/btree.c | 2202 |
btree_start_overflow_page | src/storage/btree.c | 3973 |
btree_get_new_page | src/storage/btree.c | 5135 |
btree_initialize_new_page | src/storage/btree.c | 5165 |
btree_modify_leaf_ovfl_vpid | src/storage/btree.c | 9977 |
btree_modify_overflow_link | src/storage/btree.c | 10054 |
btree_key_append_object_as_new_overflow | src/storage/btree.c | 11330 |
btree_key_append_object_to_overflow | src/storage/btree.c | 11452 |
btree_find_free_overflow_oids_page | src/storage/btree.c | 11579 |
btree_find_oid_and_its_page | src/storage/btree.c | 11646 |
btree_find_oid_from_ovfl | src/storage/btree.c | 12037 |
btree_seq_find_oid_from_ovfl | src/storage/btree.c | 12210 |
btree_overflow_remove_object | src/storage/btree.c | 32344 |
btree_overflow_record_replace_object | src/storage/btree.c | 33340 |
struct non_leaf_rec | src/storage/btree.h | 103 |
LEAF_REC | src/storage/btree.h | 110 |
ovfid | src/storage/btree.h | 129 |
btree_get_overflow_header | src/storage/btree_load.c | 340 |
btree_init_overflow_header | src/storage/btree_load.c | 576 |
btree_get_next_overflow_vpid | src/storage/btree_load.c | 675 |
btree_overflow_header | src/storage/btree_load.h | 245 |
file_init_page_type | src/storage/file_manager.c | 5349 |
file_init_temp_page_type | src/storage/file_manager.c | 5365 |
file_dealloc | src/storage/file_manager.c | 6116 |
FILE_MULTIPAGE_OBJECT_HEAP | src/storage/file_manager.h | 43 |
FILE_BTREE_OVERFLOW_KEY | src/storage/file_manager.h | 45 |
FILE_TEMP | src/storage/file_manager.h | 52 |
struct file_ovf_heap_des | src/storage/file_manager.h | 90 |
struct file_ovf_btree_des | src/storage/file_manager.h | 106 |
file_create_with_npages | src/storage/file_manager.h | 187 |
heap_ovf_find_vfid | src/storage/heap_file.c | 6462 |
heap_ovf_insert | src/storage/heap_file.c | 6569 |
heap_ovf_update | src/storage/heap_file.c | 6597 |
heap_ovf_delete | src/storage/heap_file.c | 6632 |
heap_ovf_get | src/storage/heap_file.c | 6717 |
heap_get_mvcc_rec_header_from_overflow | src/storage/heap_file.c | 19541 |
heap_set_mvcc_rec_header_on_overflow | src/storage/heap_file.c | 19567 |
heap_get_bigone_content | src/storage/heap_file.c | 19610 |
heap_delete_adjust_header | src/storage/heap_file.c | 21290 |
heap_delete_bigone | src/storage/heap_file.c | 21389 |
heap_delete_physical | src/storage/heap_file.c | 22388 |
heap_log_delete_physical | src/storage/heap_file.c | 22428 |
HEAP_HEADER_AND_CHAIN_SLOTID | src/storage/heap_file.h | 62 |
OVERFLOW_ALLOCVPID_ARRAY_SIZE | src/storage/overflow_file.c | 43 |
OVERFLOW_DO_FUNC | src/storage/overflow_file.c | 45 |
OVERFLOW_DO_DELETE | src/storage/overflow_file.c | 47 |
overflow_insert | src/storage/overflow_file.c | 81 |
overflow_insert | src/storage/overflow_file.c | 82 |
overflow_next_vpid | src/storage/overflow_file.c | 275 |
overflow_next_vpid | src/storage/overflow_file.c | 276 |
overflow_traverse | src/storage/overflow_file.c | 296 |
overflow_traverse | src/storage/overflow_file.c | 297 |
overflow_update | src/storage/overflow_file.c | 374 |
log_sysop_start | src/storage/overflow_file.c | 407 |
log_sysop_attach_to_outer | src/storage/overflow_file.c | 594 |
log_sysop_abort | src/storage/overflow_file.c | 600 |
overflow_delete_internal | src/storage/overflow_file.c | 613 |
overflow_delete_internal | src/storage/overflow_file.c | 614 |
overflow_delete | src/storage/overflow_file.c | 645 |
overflow_flush_internal | src/storage/overflow_file.c | 656 |
overflow_flush | src/storage/overflow_file.c | 678 |
overflow_get_length | src/storage/overflow_file.c | 691 |
overflow_get_length | src/storage/overflow_file.c | 692 |
overflow_get_nbytes | src/storage/overflow_file.c | 738 |
overflow_get_nbytes | src/storage/overflow_file.c | 739 |
overflow_get | src/storage/overflow_file.c | 917 |
overflow_get_capacity | src/storage/overflow_file.c | 934 |
overflow_get_capacity | src/storage/overflow_file.c | 935 |
overflow_dump | src/storage/overflow_file.c | 1039 |
overflow_rv_newpage_insert_redo | src/storage/overflow_file.c | 1113 |
overflow_rv_newpage_link_undo | src/storage/overflow_file.c | 1125 |
overflow_rv_link | src/storage/overflow_file.c | 1145 |
overflow_rv_link_dump | src/storage/overflow_file.c | 1165 |
overflow_rv_page_update_redo | src/storage/overflow_file.c | 1178 |
overflow_rv_page_update_redo | src/storage/overflow_file.c | 1179 |
overflow_rv_page_dump | src/storage/overflow_file.c | 1193 |
overflow_get_first_page_data | src/storage/overflow_file.c | 1217 |
overflow_get_first_page_data | src/storage/overflow_file.c | 1218 |
OVERFLOW_FIRST_PART | src/storage/overflow_file.h | 37 |
overflow_first_part | src/storage/overflow_file.h | 38 |
struct overflow_first_part | src/storage/overflow_file.h | 38 |
OVERFLOW_REST_PART | src/storage/overflow_file.h | 45 |
overflow_rest_part | src/storage/overflow_file.h | 46 |
struct overflow_rest_part | src/storage/overflow_file.h | 46 |
PAGE_OVERFLOW | src/storage/storage_common.h | 158 |
log_append_empty_record | src/transaction/log_manager.c | 3138 |
log_rv_copy_char | src/transaction/log_manager.c | 9087 |
LOG_DUMMY_OVF_RECORD | src/transaction/log_record.hpp | 124 |
RV_fun | src/transaction/recovery.c | 54 |
RVFL_DEALLOC | src/transaction/recovery.h | 57 |
RVFL_USER_PAGE_MARK_DELETE | src/transaction/recovery.h | 63 |
RVHF_STATS | src/transaction/recovery.h | 80 |
RVOVF_NEWPAGE_INSERT | src/transaction/recovery.h | 100 |
RVOVF_NEWPAGE_LINK | src/transaction/recovery.h | 101 |
RVOVF_PAGE_UPDATE | src/transaction/recovery.h | 102 |
RVOVF_CHANGE_LINK | src/transaction/recovery.h | 103 |
RVBT_UPDATE_OVFID | src/transaction/recovery.h | 123 |
cubrid-overflow-file.md— 상위 개요 문서. 함께 볼 문서:cubrid-heap-manager-detail.md(REC_BIGONE) 및cubrid-btree-detail.md(overflow-OID 페이지).- 코드:
src/storage/overflow_file.{c,h}; 호출부는src/storage/heap_file.c및src/storage/btree.c. - 방법론:
knowledge/methodology/code-analysis-detail-doc.md.