CUBRID MVCC — 코드 수준 심층 분석
이 문서의 위치: 상위 분석서
cubrid-mvcc.md가 설계 의도와 이론적 배경을 다룬다면, 이 문서는 코드 수준에서 모든 분기와 필드를 추적하는 심층 분석서다. 각 챕터는 독립적으로 읽을 수 있지만, 순서대로 읽으면 한 행 버전(row version)과 그 가시성을 결정하는 스냅샷이 커널 안에서 거치는 전체 생애주기를 따라갈 수 있다.
목차:
Chapter 1: 자료구조 전체 지도
섹션 제목: “Chapter 1: 자료구조 전체 지도”MVCC 모듈이 소유하는 모든 구조체를 필드 단위로 살펴본다. Snapshot Isolation
이론은 여기서 다시 유도하지 않는다 — cubrid-mvcc.md를 참고하라.
| Header | 정의된 구조체 |
|---|---|
mvcc.h | mvcc_rec_header, mvcc_snapshot, mvcc_info, 세 개의 result enum |
mvcc_active_tran.hpp | mvcc_active_tran (비트 영역 기반 active-set 엔진) |
mvcc_table.hpp | mvcc_trans_status, mvcctable (전역 코디네이터) |
storage_common.h | MVCCID typedef와 sentinel 값 사다리 |
1.1 MVCCID 타입과 sentinel 값 사다리
섹션 제목: “1.1 MVCCID 타입과 sentinel 값 사다리”부호 없는 64비트 카운터 하나가 전부다. 낮은 값들은 예약된 sentinel이며 (id 1, 2는 건너뛰고, 실제로 발급되는 첫 id는 4다).
// MVCCID + sentinels -- src/storage/storage_common.htypedef UINT64 MVCCID; /* MVCC ID */#define MVCCID_NULL (0)#define MVCCID_ALL_VISIBLE ((MVCCID) 3) /* visible for all transactions */#define MVCCID_FIRST ((MVCCID) 4)| 값 | 이름 | 역할 / 이유 |
|---|---|---|
| 0 | MVCCID_NULL | ”id 없음” — 미설정/초기화 안 된 필드 |
| 3 | MVCCID_ALL_VISIBLE | 어떤 활성 스냅샷보다도 오래된 상태. 모든 트랜잭션에게 보임 (VACUUM이 insert id를 벗겨낼 때 이 값을 채운다) |
| 4 | MVCCID_FIRST | 일반 트랜잭션이 받을 수 있는 첫 id. 카운터는 이보다 아래로 떨어지지 않는다 |
술어 매크로가 id를 분류한다 (MVCCID_IS_VALID는 != MVCCID_NULL,
MVCCID_IS_NORMAL은 >= MVCCID_FIRST). 카운터 증가 매크로는 예약 영역을
건너뛴다.
// MVCCID_FORWARD -- src/storage/storage_common.h#define MVCCID_FORWARD(id) \ do { (id)++; if ((id) < MVCCID_FIRST) (id) = MVCCID_FIRST; } while (0)불변식 — sentinel 영역 [1,2]는 실제 id가 되지 않는다.
MVCCID_FORWARD는 증가 후 값이 4보다 작으면 4로 되돌리므로, 실제 id는MVCCID_ALL_VISIBLE(3)과 절대로 충돌하지 않는다.
1.2 mvcc_rec_header — 레코드 내 MVCC 도장
섹션 제목: “1.2 mvcc_rec_header — 레코드 내 MVCC 도장”힙 객체 하나마다 함께 저장되는 헤더다. 디스크에 기록되는 유일한 MVCC 구조체이며, 나머지는 모두 메모리에만 존재한다.
// mvcc_rec_header -- src/transaction/mvcc.hstruct mvcc_rec_header{ INT32 mvcc_flag:8; INT32 repid:24; int chn; MVCCID mvcc_ins_id; MVCCID mvcc_del_id; LOG_LSA prev_version_lsa;};| 필드 | 역할 / 이유 |
|---|---|
mvcc_flag:8 | 패킹된 INT32의 하위 바이트. OR_MVCC_FLAG_* 비트가 어떤 필드가 실제로 존재하는지 가리킨다 |
repid:24 | 상위 24비트. 스키마 representation id를 같은 워드에 함께 패킹 |
chn | change counter. DELID가 없을 때 delete id 대신 기록되며, stale 캐시를 탐지한다 |
mvcc_ins_id | 탄생 도장. inserter가 내 스냅샷 이전에 커밋했는가를 결정 |
mvcc_del_id | 사망 도장. deleter가 내 스냅샷 이전에 커밋했는가를 결정 |
prev_version_lsa | 버전 체인 링크. 너무 새로운 레코드는 이 LSA를 따라 거슬러 올라간다 |
OR_MVCC_FLAG_* 비트(object_representation_constants.h에 정의됨)는 어떤
필드가 유효한지를 표시한다 — VALID_INSID/VALID_DELID/VALID_PREV_VERSION이
OR_MVCC_FLAG_MASK 아래에 묶여 있다.
불변식 —
DELID와chn은 물리적 union이 아니라 논리적으로 배타적이다. 디스크 헤더는 둘 중 하나만 기록한다 (OR_MVCC_FLAG_VALID_DELID가 선택을 결정한다).MVCC_IS_HEADER_DELID_VALID는 이 플래그와MVCCID_IS_VALID를 모두 확인하므로,chn이 트랜잭션 id로 잘못 해석되는 일은 일어나지 않는다.
초기화 매크로는 플래그와 repid를 0으로 두고 chn을 NULL_CHN (-1)로, 두
id를 MVCCID_NULL로, LSA를 null로 만든다.
// MVCC_REC_HEADER_INITIALIZER -- src/transaction/mvcc.h#define MVCC_REC_HEADER_INITIALIZER \{ 0, 0, NULL_CHN, MVCCID_NULL, MVCCID_NULL, LSA_INITIALIZER }MVCC_IS_HEADER_ALL_VISIBLE은 VACUUM이 깨끗하게 정리한 경우를 식별한다 —
insid/delid 플래그가 모두 꺼져 있고 mvcc_ins_id == MVCCID_ALL_VISIBLE인 상태다.
1.3 mvcc_active_tran — 비트 영역 active-set
섹션 제목: “1.3 mvcc_active_tran — 비트 영역 active-set”“MVCCID x가 아직 활성인가?”라는 질문에 lock-free로 답한다. mvcc_snapshot과
mvcc_trans_status 양쪽에 값으로 포함되어 있다. Chapter 4에서 탐색 방식을
자세히 다룬다.
// mvcc_active_tran private state -- src/transaction/mvcc_active_tran.hppusing unit_type = std::uint64_t;static const size_t BITAREA_MAX_SIZE = 500;static const unit_type ALL_ACTIVE = 0;static const unit_type ALL_COMMITTED = (unit_type) -1;
unit_type *m_bit_area;volatile MVCCID m_bit_area_start_mvccid;volatile size_t m_bit_area_length;MVCCID *m_long_tran_mvccids;volatile size_t m_long_tran_mvccids_length;bool m_initialized;| 필드 | 역할 / 이유 |
|---|---|
m_bit_area | 64비트 워드 배열. id당 비트 한 개. set = 완료, clear = 활성 |
m_bit_area_start_mvccid | 비트 0이 가리키는 id. 윈도의 왼쪽 끝. offset = id - start |
m_bit_area_length | 길이 (비트 단위). 오래된 id가 완료되면 왼쪽부터 잘려나간다 |
m_long_tran_mvccids | 왼쪽 끝 너머에 있는, 여전히 활성인 id들의 overflow 배열. 윈도가 미끄러질 수 있게 한다 |
m_long_tran_mvccids_length | overflow 스캔 범위 |
m_initialized | 기본 생성된 상태와 살아있는 상태를 구분하는 가드. finalize/reset에서 사용 |
ALL_ACTIVE (0)와 ALL_COMMITTED ((unit_type)-1)는 각각 모두 0인 워드,
모두 1인 워드 패턴이다. BITAREA_MAX_SIZE (500 워드)는 윈도를
500 × 64 = 32 000개 id로 제한하며, 그 너머는 overflow로 흘러간다.
불변식 — 비트 offset은 항상
[0, m_bit_area_length)범위 안에 있다. Chapter 4의 탐색 루틴은m_bit_area에 접근하기 전 범위를 확인한다. 그렇지 않으면 500워드 영역 너머를 읽어 들이게 된다.volatile이 붙은 윈도 필드 덕에 reader는 committer가 윈도를 미끄러뜨리는 동안에도(start, length)쌍을 일관되게 스냅샷할 수 있다.
operator=는 delete 되어 있다 — m_bit_area가 얕은 복사되는 일을
원천적으로 막기 위함이다. 호출자는 copy_to를 쓰며, 이 함수는
enum class copy_safety { THREAD_SAFE, THREAD_UNSAFE } 인자로 안전 모드를
선택한다.
1.4 mvcc_snapshot — 트랜잭션의 동결된 뷰
섹션 제목: “1.4 mvcc_snapshot — 트랜잭션의 동결된 뷰”읽기가 시작될 때 “누가 커밋되어 있었는가”의 불변의 사진이다.
// mvcc_snapshot -- src/transaction/mvcc.hstruct mvcc_snapshot{ MVCCID lowest_active_mvccid; MVCCID highest_completed_mvccid; mvcc_active_tran m_active_mvccs; MVCC_SNAPSHOT_FUNC snapshot_fnc; bool valid; // ... mvcc_snapshot() ctor + reset() omitted ... mvcc_snapshot &operator= (const mvcc_snapshot& snapshot) = delete; void copy_to (mvcc_snapshot & dest) const;};| 필드 | 역할 / 이유 |
|---|---|
lowest_active_mvccid | 하한 워터마크. 이 값보다 작은 id는 비트를 안 봐도 커밋되어 보인다 |
highest_completed_mvccid | 상한 워터마크. 이 값보다 큰 id는 비트를 안 봐도 보이지 않는다 |
m_active_mvccs | 임베디드 mvcc_active_tran. 두 워터마크 사이의 정확한 답 |
snapshot_fnc | 교체 가능한 술어. 같은 스냅샷으로 일반 읽기와 dirty 읽기를 모두 처리 |
valid | false면 재사용된 tdes 슬롯에서 reset된 상태. 재구축을 유발 |
MVCC_SNAPSHOT_FUNC는 (*)(THREAD_ENTRY *, MVCC_REC_HEADER *, MVCC_SNAPSHOT *)
시그니처로 MVCC_SATISFIES_SNAPSHOT_RESULT를 반환한다.
불변식 — 두 워터마크가 비트 영역을 감싼다 (
lowest_active_mvccid <= highest_completed_mvccid + 1이며, 비트 영역은 둘 사이에서만 조회된다).operator=가 삭제되어 있어 모든 복사가copy_to를 거치게 강제되고, 이때 비트가 deep-copy되므로 워터마크와 비트의 일관성이 유지된다.
1.5 mvcc_info — tdes마다 붙는 트랜잭션 MVCC 상태
섹션 제목: “1.5 mvcc_info — tdes마다 붙는 트랜잭션 MVCC 상태”활성 트랜잭션 디스크립터(log_tdes)당 하나씩 존재한다.
// mvcc_info -- src/transaction/mvcc.hstruct mvcc_info{ MVCC_SNAPSHOT snapshot; MVCCID id; MVCCID recent_snapshot_lowest_active_mvccid; std::vector<MVCCID> sub_ids; bool is_sub_active; // ... mvcc_info() ctor + init() + reset() omitted ... void copy_to (mvcc_info & dest) const;};| 필드 | 역할 / 이유 |
|---|---|
snapshot | 현재 mvcc_snapshot (값으로 보관). RC에서는 문장마다 재구축, SR에서는 유지 |
id | 이 트랜잭션의 MVCCID. 첫 쓰기 전까지는 MVCCID_NULL. 지연 할당 (Ch 3) |
recent_snapshot_lowest_active_mvccid | 캐시된 하한 워터마크. “확실히 비활성”의 빠른 컷오프 — is_active가 전역 테이블을 건너뛸 수 있게 함 |
sub_ids | sub-transaction id 배열. 중첩될 때마다 하나씩 (Ch 10) |
is_sub_active | sub-transaction이 열려 있는 동안 true. 가시성 판정이 sub_ids도 함께 확인하게 만든다 |
1.6 mvcc_trans_status — active-set의 불변 히스토리 스냅샷
섹션 제목: “1.6 mvcc_trans_status — active-set의 불변 히스토리 스냅샷”전역 테이블은 버전이 매겨진 상태 레코드들의 ring을 유지한다. 각 슬롯은
mvcc_trans_status 한 개다.
// mvcc_trans_status -- src/transaction/mvcc_table.hppstruct mvcc_trans_status{ using version_type = unsigned int; enum event_type { COMMIT, ROLLBACK, SUBTRAN };
mvcc_active_tran m_active_mvccs; MVCCID m_last_completed_mvccid; // just for info event_type m_event_type; // just for info std::atomic<version_type> m_version;};| 필드 | 역할 / 이유 |
|---|---|
m_active_mvccs | 이 버전 시점의 active-set 비트 |
m_last_completed_mvccid | 진단용. 이 버전에서 마지막으로 완료된 id |
m_event_type | 진단용. COMMIT / ROLLBACK / SUBTRAN 태그 |
m_version | seqlock의 핵심. 읽고-복사하고-다시 읽었을 때 값이 같으면 일관된 복사 (Ch 5) |
불변식 —
m_version이 일관된 복사의 시작과 끝을 표시한다. committer는 비트를 변경하기 전후로 이 값을 올린다. reader가 복사 전후로 같은 버전을 보았다면, 중간에 다른 쓰기가 끼어들지 않았음을 알 수 있다 (Ch 5). 진단용 두 필드는 이 계약과 관련이 없다.
1.7 mvcctable — 전역 코디네이터
섹션 제목: “1.7 mvcctable — 전역 코디네이터”log_Gl.mvcc_table에 인스턴스가 하나 존재한다. id 할당, 상태 히스토리 ring,
트랜잭션별 lowest-visible 배열, VACUUM을 구동하는 oldest-visible 워터마크를
모두 소유한다 (lowest_active_mvccid_type은 std::atomic<MVCCID>이다).
// mvcctable private members -- src/transaction/mvcc_table.hppstatic const size_t HISTORY_MAX_SIZE = 2048; // must be a power of 2static const size_t HISTORY_INDEX_MASK = HISTORY_MAX_SIZE - 1;
lowest_active_mvccid_type *m_transaction_lowest_visible_mvccids; /* size = NUM_TOTAL_TRAN_INDICES */size_t m_transaction_lowest_visible_mvccids_size;lowest_active_mvccid_type m_current_status_lowest_active_mvccid;
mvcc_trans_status m_current_trans_status;std::atomic<size_t> m_trans_status_history_position;mvcc_trans_status *m_trans_status_history; /* ring of HISTORY_MAX_SIZE */
std::mutex m_new_mvccid_lock;std::mutex m_active_trans_mutex;
std::atomic<MVCCID> m_oldest_visible;std::atomic<size_t> m_ov_lock_count;| 필드 | 역할 / 이유 |
|---|---|
m_transaction_lowest_visible_mvccids | 트랜잭션별 atomic 배열. 각 tran이 보존해야 하는 가장 오래된 id를 게시. 최솟값이 oldest-visible로 흘러간다 (Ch 9) |
m_transaction_lowest_visible_mvccids_size | 최솟값 스캔 한계. = NUM_TOTAL_TRAN_INDICES |
m_current_status_lowest_active_mvccid | 현재 status의 atomic 하한 워터마크. 스냅샷의 빠른 하한 |
m_current_trans_status | 살아 있는 최신 status. m_active_trans_mutex 하에서만 변경 |
m_trans_status_history_position | 최신 슬롯을 가리키는 atomic ring 커서. HISTORY_INDEX_MASK로 마스킹 |
m_trans_status_history | HISTORY_MAX_SIZE 길이의 ring. reader가 안정적인 과거 버전을 잡아간다 (Ch 5/8) |
m_new_mvccid_lock | get_new_mvccid / get_two_new_mvccid를 직렬화 — 단조 id 보장 |
m_active_trans_mutex | 현재 status 변경과 history 전진을 직렬화 |
m_oldest_visible | 전역 워터마크. VACUUM은 이보다 새 버전을 회수하지 못한다 |
m_ov_lock_count | soft pin. 0이 아니면 m_oldest_visible을 동결해 안정된 floor를 만든다 (Ch 9) |
불변식 —
HISTORY_MAX_SIZE는 2의 거듭제곱이다. ring 인덱싱pos & HISTORY_INDEX_MASK가 올바른 모듈로가 되려면 이 조건이 필요하다. 그렇지 않으면 마스크가 인접하지 않은 슬롯을 같은 인덱스로 만들어 reader가 잘못된 과거 버전을 받는다.
불변식 —
m_oldest_visible은 unpin 상태에서만 앞으로 움직인다 (단조 증가,m_ov_lock_count > 0일 때는 동결). 이를 어기면 VACUUM이 활성 reader에게 필요한 버전을 회수해 행이 사라지는 사태가 벌어진다 (Ch 9).
1.8 세 개의 result enum
섹션 제목: “1.8 세 개의 result enum”가시성 판정은 bool이 아니라 타입화된 enum을 돌려준다 — 그래야 호출자가 “너무 오래된” 경우와 “너무 새로운” 경우를 구분할 수 있다 (후자는 버전 체인을 따라 거슬러 올라간다).
// mvcc_satisfies_snapshot_result -- src/transaction/mvcc.henum mvcc_satisfies_snapshot_result{ TOO_OLD_FOR_SNAPSHOT, SNAPSHOT_SATISFIED, TOO_NEW_FOR_SNAPSHOT };TOO_OLD_FOR_SNAPSHOT은 “나에게는 죽었다, 멈춰라”라는 뜻이며,
SNAPSHOT_SATISFIED는 “읽어라”라는 뜻이고, TOO_NEW_FOR_SNAPSHOT은 “내 스냅샷
이후 태어났다, prev_version_lsa를 따라가라”라는 뜻이다.
// mvcc_satisfies_delete_result -- src/transaction/mvcc.henum mvcc_satisfies_delete_result{ DELETE_RECORD_INSERT_IN_PROGRESS, DELETE_RECORD_CAN_DELETE, DELETE_RECORD_DELETED, DELETE_RECORD_DELETE_IN_PROGRESS, DELETE_RECORD_SELF_DELETED};INSERT_IN_PROGRESS는 inserter가 아직 커밋되지 않았으므로 건드릴 수 없다는
뜻이다. CAN_DELETE는 깨끗하다, DELETED는 다른 tran이 삭제를 커밋했다,
DELETE_IN_PROGRESS는 살아있는 tran이 잡고 있으니 기다리거나 중단하라,
SELF_DELETED는 이 트랜잭션 내에서 내가 삭제했다는 뜻이다.
// mvcc_satisfies_vacuum_result -- src/transaction/mvcc.henum mvcc_satisfies_vacuum_result{ VACUUM_RECORD_REMOVE, VACUUM_RECORD_DELETE_INSID_PREV_VER, VACUUM_RECORD_CANNOT_VACUUM};REMOVE는 죽은 버전을 회수한다는 뜻이다. DELETE_INSID_PREV_VER는 행은
유지하되 쓸모없어진 insert id와 prev-version 링크를 떼어낸다는 뜻이고,
CANNOT_VACUUM은 아직 회수할 수 없다는 뜻이다. 이 enum들은 Chapters 6–7에서
그것들을 생성하는 함수들과 짝지어 자세히 분석한다.
1.9 서로를 어떻게 가리키는가
섹션 제목: “1.9 서로를 어떻게 가리키는가”log_Gl에 mvcctable 하나, tdes마다 mvcc_info 하나, 그리고 디스크에 있는
mvcc_rec_header. 가시성은 이 mvcc_info 안의 스냅샷을 헤더와 비교한다.
임베디드된 모든 mvcc_active_tran은 공유 포인터가 아니라 독립된 값 복사본이다 —
그래서 reader가 안정된 스냅샷을 들고 있는 동안에도 전역 status는 앞으로
나아갈 수 있다.
graph TD
subgraph Global["log_Gl"]
MT["mvcctable"]
MT --> CUR["m_current_trans_status"]
MT --> RING["m_trans_status_history[2048]"]
MT --> LVA["m_transaction_lowest_visible_mvccids[]"]
MT --> OV["m_oldest_visible + m_ov_lock_count"]
CUR --> CAT["m_active_mvccs (값으로)"]
RING --> RAT["m_active_mvccs (값으로)"]
end
subgraph Tdes["log_tdes (트랜잭션마다)"]
MI["mvcc_info"]
MI --> SNAP["snapshot (mvcc_snapshot)"]
MI --> ID["id"]
MI --> SUB["sub_ids + is_sub_active"]
SNAP --> SAT["m_active_mvccs (값으로)"]
SNAP --> WM["lowest/highest 워터마크"]
end
subgraph Disk["힙 레코드 (디스크)"]
RH["mvcc_rec_header"]
RH --> INS["mvcc_ins_id / mvcc_del_id"]
RH --> PV["prev_version_lsa"]
end
MT -. "build_mvcc_info가 비트와 워터마크를 복사" .-> SNAP
SNAP -. "snapshot_fnc가 헤더를 비교" .-> RH
PV -. "버전 체인 탐색" .-> Disk
Figure 1-1. 실선은 containment (별도 표시 없으면 값으로 보관), 점선은 런타임
데이터 흐름이다. 테이블이 스냅샷을 채워주고, snapshot_fnc가 헤더를
평가하고, 너무 새로운 레코드는 prev_version_lsa를 따라간다.
1.10 Cross-check 노트
섹션 제목: “1.10 Cross-check 노트”- 레거시 매크로
MVCC_IS_REC_DELETED_BY는mvcc_rec_header에서 사라진delid_chn.mvcc_del_idunion 멤버를 여전히 dereference한다 (현재 헤더는chn과mvcc_del_id가 별도 필드다). 죽은 코드이며, 실제 읽기는MVCC_GET_DELID/MVCC_IS_HEADER_DELID_VALID를 사용한다.
1.11 챕터 요약 — 핵심 정리
섹션 제목: “1.11 챕터 요약 — 핵심 정리”MVCCID는 하위에 예약 영역을 두는 64비트 카운터다 (0/3/4 =MVCCID_NULL/MVCCID_ALL_VISIBLE/MVCCID_FIRST).MVCCID_FORWARD가 실제 id를 이 영역 위로만 유지한다.mvcc_rec_header는 디스크에 기록되는 유일한 MVCC 구조체다.mvcc_flag:8이mvcc_ins_id,mvcc_del_id(vschn),prev_version_lsa중 어느 것이 존재하는지를 결정한다.DELID와chn은 디스크에서 배타적이지만 메모리에선 별개의 필드다.mvcc_active_tran은 슬라이딩 비트 윈도 + long-tran overflow 배열이다. offset은id - m_bit_area_start_mvccid로 계산하며, 윈도 크기는 최대 500 × 64 id다.volatile필드가 lock-free 읽기를 가능하게 한다.mvcc_snapshot은 두 워터마크로 비트 영역을 감싸고, 교체 가능한snapshot_fnc를 받는다. 복사는 오직copy_to를 통해서만 이루어진다.mvcc_info는tdes마다 스냅샷, 자신의id, 빠른recent_snapshot_lowest_active_mvccid컷오프, sub-transaction 상태를 묶어주는 묶음이다.mvcc_trans_status는m_version만으로 검증되는 seqlock-versioned active-set 스냅샷이다. 나머지 두 필드는 진단용이다.mvcctable은 단일 전역 코디네이터다 — id 발급, 2의 거듭제곱 history ring, lowest-visible 배열, 그리고 VACUUM을 게이트하는 단조-증가 pinnablem_oldest_visible워터마크를 소유한다.
Chapter 2: 초기화와 메모리 배치
섹션 제목: “Chapter 2: 초기화와 메모리 배치”이 챕터의 독자 질문은 이렇다 — 어떤 트랜잭션이 시작되기 전에 각 MVCC 구조체는
어떻게 할당되고, 크기가 정해지고, 부트스트랩되는가? 그리고 그 magic 크기들은
어디서 오는가? Chapter 1에서 세 개의 소유 객체를 지도로 그렸다. 여기서는 모든
생성자, initialize/finalize, reset*, 크기 헬퍼를 추적한다. 가시성 이론은
cubrid-mvcc.md에 있다.
MVCCID 카운터는 이 세 구조체 어디에도 들어 있지 않다 — log 헤더의
log_Gl.hdr.mvcc_next_id (log_storage.hpp)에 있으며, 테이블은
m_new_mvccid_lock 하에서 이 값을 읽고 전진시키기만 한다. 자신의 파생
시작 마커는 여기서 시드받는다. 이 점이 2.6의 핵심이다. 이 챕터에서 다루는
구조체들은 오로지 파생된 상태만을 보관한다.
2.1 두 가지 크기 축
섹션 제목: “2.1 두 가지 크기 축”- 동적, tran-index당 —
logtb_get_number_of_total_tran_indices ()(=log_Gl.trantable.num_total_indices) 길이:m_long_tran_mvccids와m_transaction_lowest_visible_mvccids. tran 테이블이 커지면 크기가 다시 잡힌다. - 정적, 컴파일 시점 고정 — 비트 영역
BITAREA_MAX_SIZE = 500워드, ringHISTORY_MAX_SIZE = 2048슬롯. 절대 리사이즈하지 않으며, overflow는 이전 (비트 영역에서 long-tran 배열로)이거나 덮어쓰기 (ring이 한 바퀴 도는 것)다. 재할당은 절대 없다.
// mvcc_active_tran (private) -- src/transaction/mvcc_active_tran.hppusing unit_type = std::uint64_t;static const size_t BITAREA_MAX_SIZE = 500; // 500 units, fixedstatic const size_t UNIT_BIT_COUNT = sizeof (unit_type) * BYTE_BIT_COUNT; // 64static const size_t BITAREA_MAX_MEMSIZE = BITAREA_MAX_SIZE * UNIT_BYTE_COUNT; // 4000 bytesstatic const size_t BITAREA_MAX_BITS = BITAREA_MAX_SIZE * UNIT_BIT_COUNT; // 32000 bitsstatic const unit_type ALL_ACTIVE = 0;static const unit_type ALL_COMMITTED = (unit_type) -1;unit_type 한 개가 64비트이므로, 비트 영역은 500 워드 = 4000 바이트이며
32000개 MVCCID를 추적한다. ALL_ACTIVE = 0이라는 점이 결정적이다 —
new[]()로 0으로 초기화된 버퍼가 이미 “모든 슬롯이 활성”을 의미하므로
init 단계에서 비트를 따로 비울 필요가 없다. 헬퍼들이 세 가지 단위를 서로
변환한다.
// mvcc_active_tran helpers -- src/transaction/mvcc_active_tran.cppsize_t bit_size_to_unit_size (size_t b) { return (b + UNIT_BIT_COUNT - 1) / UNIT_BIT_COUNT; } // bits->words, ceilsize_t units_to_bits (size_t n) { return n * UNIT_BIT_COUNT; } // words -> bitssize_t units_to_bytes (size_t n) { return n * UNIT_BYTE_COUNT; } // words -> bytessize_t get_area_size () const { return bit_size_to_unit_size (m_bit_area_length); } // LIVE wordssize_t get_bit_area_memsize () const { return units_to_bytes (get_area_size ()); } // LIVE bytesget_area_size()는 살아 있는 워드 수다 (m_bit_area_length에서 계산).
BITAREA_MAX_SIZE는 할당된 워드 수다. 버퍼는 항상 최대 폭으로 잡혀 있으며,
reset과 복사 경로는 get_bit_area_memsize() 바이트만 건드린다 — 따라서 그
뒤에 오는 워드들은 항상 ALL_ACTIVE를 유지해야 한다.
flowchart LR
subgraph mvcctable["mvcctable (log_Gl당 하나)"]
A["m_transaction_lowest_visible_mvccids\natomic MVCCID [ total_tran_indices ]"]
B["m_trans_status_history\nmvcc_trans_status [ 2048 ]"]
C["m_current_trans_status\nmvcc_trans_status"]
end
C --> F["m_active_mvccs : mvcc_active_tran"]
B --> G["[i].m_active_mvccs : mvcc_active_tran"]
F --> H["m_bit_area : unit_type [500]\nm_long_tran_mvccids : MVCCID [ total_tran_indices ]"]
G --> H
Figure 2-1. 소유 관계와 크기 축. 두 MVCCID 배열은 동적 축이며, [500] 비트
영역과 [2048] ring은 정적 축이다. 각 mvcc_trans_status (current + 2048
ring 슬롯)가 mvcc_active_tran을 통째로 임베드하므로, 살아있는 테이블은
2049 개의 비트 영역을 들고 있다.
2.2 mvcc_active_tran: 생성, initialize, finalize, reset
섹션 제목: “2.2 mvcc_active_tran: 생성, initialize, finalize, reset”기본 생성자는 비어 있고 초기화되지 않은 객체를 만든다 — 힙 없음, 포인터는
NULL, 시작 마커는 MVCCID_FIRST. initialize가 이 구조체의 유일한 두 번의
힙 할당을 수행하며, idempotency를 위한 가드가 걸려 있다.
// mvcc_active_tran::mvcc_active_tran -- src/transaction/mvcc_active_tran.cppmvcc_active_tran::mvcc_active_tran () : m_bit_area (NULL) , m_bit_area_start_mvccid (MVCCID_FIRST) /* <- 4, never 0 */ , m_bit_area_length (0) , m_long_tran_mvccids (NULL) , m_long_tran_mvccids_length (0) , m_initialized (false) { }
// mvcc_active_tran::initialize -- src/transaction/mvcc_active_tran.cppvoid mvcc_active_tran::initialize (){ if (m_initialized) { return; } /* <- branch 1: already up, no-op */ m_bit_area = new unit_type[BITAREA_MAX_SIZE] (); /* <- () zero-inits => ALL_ACTIVE */ m_bit_area_start_mvccid = MVCCID_FIRST; m_bit_area_length = 0; m_long_tran_mvccids = new MVCCID[long_tran_max_size ()] (); /* <- sized to tran indices */ m_long_tran_mvccids_length = 0; m_initialized = true;}뒤에 붙는 ()는 두 배열을 0으로 값-초기화한다. ALL_ACTIVE == 0이므로 이것이
바로 “모두 활성, 길이 0”의 올바른 상태가 된다. long_tran_max_size ()는
logtb_get_number_of_total_tran_indices ()를 반환한다 — 동시에 활성일 수 있는
“long” 트랜잭션 (즉 m_bit_area_start_mvccid보다 오래된 것들)의 상한이다.
add_long_transaction은 m_long_tran_mvccids_length < long_tran_max_size ()를
assert한다.
finalize는 두 배열을 해제하고, 포인터를 null로 만들고, 플래그를 떨어뜨린다 —
~mvcc_active_tran과 달리 객체를 다시 initialize할 수 있도록 상태를 reset
한다. reset과 reset_active_transactions는 finalize가 아니다 — 둘 다
할당은 유지한 채 내용만 지운다 (스냅샷 복사 재시도 경로, Chapter 5). 둘은
다음과 같이 다르다. reset은 살아 있는 prefix만 (get_bit_area_memsize (),
0바이트 호출은 막혀 있다) memset하고, 시작 마커를 MVCCID_NULL로 되돌린다.
reset_active_transactions는 **전체 BITAREA_MAX_MEMSIZE**를 memset하고 시작
마커는 그대로 둔다. 전체를 지우는 이유는, lock-free 복사가 실패했을 때 (복사
중간에 버전이 바뀐 경우) prefix 너머에 쓰레기 데이터가 남았을 수 있고, prefix만
지우는 것으로는 그것을 놓치기 때문이다.
// mvcc_active_tran::finalize -- src/transaction/mvcc_active_tran.cppvoid mvcc_active_tran::finalize (){ delete [] m_bit_area; m_bit_area = NULL; delete [] m_long_tran_mvccids; m_long_tran_mvccids = NULL; m_initialized = false; }
// mvcc_active_tran::reset -- src/transaction/mvcc_active_tran.cppvoid mvcc_active_tran::reset (){ if (!m_initialized) { return; } /* <- branch 1: bare object => no-op */ if (m_bit_area_length > 0) /* <- branch 2: memset only LIVE prefix */ { std::memset (m_bit_area, 0, get_bit_area_memsize ()); } m_bit_area_length = 0; m_bit_area_start_mvccid = MVCCID_NULL; /* <- NULL (0), not MVCCID_FIRST */ m_long_tran_mvccids_length = 0; check_valid ();}
// mvcc_active_tran::reset_active_transactions -- src/transaction/mvcc_active_tran.cppvoid mvcc_active_tran::reset_active_transactions (){ std::memset (m_bit_area, 0, BITAREA_MAX_MEMSIZE); /* <- full 4000 bytes */ m_bit_area_length = 0; m_long_tran_mvccids_length = 0; }불변식 (trailing-words-clear).
get_unit_of(m_bit_area_length) + 1부터m_bit_area + BITAREA_MAX_SIZE까지의 모든 워드는ALL_ACTIVE(0)와 같고, 마지막 부분 워드에서m_bit_area_length너머의 비트는 0이다. 강제 수단:check_valid(#ifndef NDEBUG루프가*p_area == ALL_ACTIVE를 assert). 유지자:initialize,reset,ltrim_area,reset_active_transactions. 위반 시:compute_lowest_active_mvccid/compute_highest_completed_mvccid가 살아 있는 길이 너머의 stale set-bit를 읽고 잘못된 워터마크를 보고하여 가시성을 손상시킨다.
구조체: mvcc_active_tran — 모든 필드
섹션 제목: “구조체: mvcc_active_tran — 모든 필드”| Field | Role | Why it exists |
|---|---|---|
m_bit_area | BITAREA_MAX_SIZE 워드 윈도. bit 0 = 활성, 1 = 커밋됨 | 최근 MVCCID당 한 비트, 워드당 64개 |
m_bit_area_start_mvccid (volatile MVCCID) | 비트 offset 0에 매핑된 MVCCID | 윈도를 절대 좌표에 고정 |
m_bit_area_length (volatile size_t) | 사용 중인 비트 수 (바이트도 alloc 크기도 아니다) | 스캔/memset의 범위 |
m_long_tran_mvccids | 윈도 아래에 있는 활성 MVCCID들의 오름차순 배열 | 오래된 트랜잭션의 overflow 저장소 |
m_long_tran_mvccids_length (volatile size_t) | long-tran 배열의 살아있는 항목 수 | 스캔 범위. < long_tran_max_size() assert |
m_initialized | initialize 실행됐고 아직 finalize 안 됐는지 | idempotent init, 안전한 no-op reset |
세 필드의 volatile은 m_active_trans_mutex 하에서 변경되는 active-set을
lock-free로 읽기 위한 표시다. 실제 펜스는 build_mvcc_info의 버전 재확인이
담당한다 (Chapter 5).
2.3 mvcc_trans_status: 버전 태그된 래퍼
섹션 제목: “2.3 mvcc_trans_status: 버전 태그된 래퍼”얇은 봉투다 — mvcc_active_tran 하나, 약간의 부기 정보, 그리고 reader가
spin하는 atomic 버전 카운터. 생성자는 버전을 0으로 설정하고 정보 필드를
중립값으로 둔다. initialize는 하위로 위임한 뒤 버전을 다시 0으로 찍는다 (재사용된
객체에서는 이전 버전이 올라가 있을 수 있다). finalize는 이를 거울처럼 따라간다.
// mvcc_trans_status -- src/transaction/mvcc_table.hppstruct mvcc_trans_status{ using version_type = unsigned int; enum event_type { COMMIT, ROLLBACK, SUBTRAN }; mvcc_active_tran m_active_mvccs; MVCCID m_last_completed_mvccid; // just for info event_type m_event_type; // just for info std::atomic<version_type> m_version;};// ctor / initialize / finalize -- src/transaction/mvcc_table.cppmvcc_trans_status::mvcc_trans_status () : m_active_mvccs () , m_last_completed_mvccid (MVCCID_NULL) , m_event_type (COMMIT) , m_version (0) { }void mvcc_trans_status::initialize () { m_active_mvccs.initialize (); m_version = 0; }void mvcc_trans_status::finalize () { m_active_mvccs.finalize (); }구조체: mvcc_trans_status — 모든 필드
섹션 제목: “구조체: mvcc_trans_status — 모든 필드”| Field | Role | Why it exists |
|---|---|---|
m_active_mvccs | 이 스냅샷의 active-set 비트맵 | 본체. 나머지는 메타데이터 |
m_last_completed_mvccid | 여기서 마지막으로 커밋/롤백된 MVCCID | 정보/디버그용. 가시성은 무시 |
m_event_type | COMMIT/ROLLBACK/SUBTRAN | 정보용. ring 전진의 디버거 추적 |
m_version (atomic) | 상태가 바뀔 때마다 단조 증가하는 도장 | lock-free 가드 — 다시 읽었을 때 그대로면 일관된 복사 |
2.4 mvcctable::initialize: 2048 슬롯 ring과 트랜잭션별 배열
섹션 제목: “2.4 mvcctable::initialize: 2048 슬롯 ring과 트랜잭션별 배열”mvcctable은 유일한 소유 객체다 (log_Gl.mvcc_table). 생성자는 모든 멤버를
무해한 기본값으로 연결하지만 아무것도 할당하지 않는다 (마커는
MVCCID_FIRST/MVCCID_NULL, 포인터는 NULL, 크기와 카운트는 0). initialize
(부팅 시 logtb_define_trantable_log_latch에서 한 번 호출)가 할당을 수행한다.
// mvcctable::initialize -- src/transaction/mvcc_table.cppvoid mvcctable::initialize (){ m_current_trans_status.initialize (); /* <- 1: seed the live status */ m_trans_status_history = new mvcc_trans_status[HISTORY_MAX_SIZE]; /* <- 2: 2048 slots, ctors only */ for (size_t idx = 0; idx < HISTORY_MAX_SIZE; idx++) { m_trans_status_history[idx].initialize (); } /* <- 3: each slot allocs its bit area */ m_trans_status_history_position = 0; /* <- 4: ring head at slot 0 */ m_current_status_lowest_active_mvccid = MVCCID_FIRST; /* <- 5: nothing older than 4 active */ alloc_transaction_lowest_active (); /* <- 6: per-tran array */}단계 순서가 의도적이다. 그냥 new[2048]을 부르면 생성자만 실행되고 (임베디드된
active-set 포인터는 NULL이다), 각 슬롯별 initialize가 실제 힙 작업을 한다.
따라서 살아 있는 테이블은 2049개의 mvcc_active_tran 인스턴스를 들고 있다
(현재 status + 2048 ring 슬롯), 각각 4000바이트 버퍼다.
HISTORY_MAX_SIZE = 2048은 2의 거듭제곱이라 ring이 & HISTORY_INDEX_MASK
(= 2047)로 wrap되고 모듈로가 필요 없다.
불변식 (history-position-in-range).
m_trans_status_history_position < HISTORY_MAX_SIZE는 항상 성립한다. 강제 수단: init 시 0, 전진 시(pos + 1) & HISTORY_INDEX_MASK로[0, 2047]안에 마스킹.build_mvcc_info와is_active는assert (index < HISTORY_MAX_SIZE)를 한다. 위반 시: reader가 ring 너머로 인덱싱하여 무관한 메모리를 active-set으로 읽는다.
finalize는 역순으로 정리하면서 per-tran 크기를 0으로 두어 다음 init에서
재할당하게 한다. ring 슬롯을 따로 루프하지 않는다 —
delete [] m_trans_status_history가 각 ~mvcc_trans_status를 호출하고 그것이
~mvcc_active_tran을 호출하므로 모든 슬롯의 버퍼가 해제된다. 현재 status의
비트 영역은 명시적으로 m_current_trans_status.finalize ()로 해제한다.
// mvcctable::finalize -- src/transaction/mvcc_table.cppvoid mvcctable::finalize (){ m_current_trans_status.finalize (); delete [] m_trans_status_history; m_trans_status_history = NULL; delete [] m_transaction_lowest_visible_mvccids; m_transaction_lowest_visible_mvccids = NULL; m_transaction_lowest_visible_mvccids_size = 0; /* <- forces re-alloc on next init */}flowchart TD
S(["mvcctable::initialize"]) --> A["m_current_trans_status.initialize()\n=> allocs current bit area + long-tran array"]
A --> B["new mvcc_trans_status[2048]\n=> 2048 default ctors, arrays still NULL"]
B --> C{"loop idx 0..2047"}
C -->|each| D["history[idx].initialize()\n=> allocs that slot's bit area + long-tran array"]
C -->|done| E["history_position = 0"]
E --> F["current_status_lowest_active = MVCCID_FIRST"]
F --> G(["alloc_transaction_lowest_active()"])
Figure 2-2. mvcctable::initialize 제어 흐름.
2.5 alloc_transaction_lowest_active: 크기 변경 감지
섹션 제목: “2.5 alloc_transaction_lowest_active: 크기 변경 감지”한 번 이상 실행될 수 있는 유일한 할당이다 — 부팅 시 한 번, 그리고 트랜잭션 테이블이 리사이즈될 때마다. 리사이즈-감지 체크 형태로 작성되어 있다.
// mvcctable::alloc_transaction_lowest_active -- src/transaction/mvcc_table.cppvoid mvcctable::alloc_transaction_lowest_active (){ if (m_transaction_lowest_visible_mvccids_size != (size_t) logtb_get_number_of_total_tran_indices ()) { // first time or tran table resized delete [] m_transaction_lowest_visible_mvccids; /* <- delete NULL is fine first time */ m_transaction_lowest_visible_mvccids_size = logtb_get_number_of_total_tran_indices (); m_transaction_lowest_visible_mvccids = new lowest_active_mvccid_type[m_transaction_lowest_visible_mvccids_size] (); // all 0 = MVCCID_NULL }}두 갈래다. (1) 크기가 일치하면 본문이 건너뛰어진다 (예를 들면
logtb_expand_trantable이 변경 없이 다시 호출된 경우 같은, 정상 상태에서
중복 호출되는 케이스다). (2) 크기가 다르면 (첫 호출은 저장 크기가 0이고,
또는 실제 리사이즈) 기존 배열이 해제되고 (delete [] NULL은 합법적이다)
크기가 업데이트된 뒤 새 lowest_active_mvccid_type (= std::atomic<MVCCID>)
배열이 값-초기화된다. 따라서 모든 원소가 MVCCID_NULL (0) = “스냅샷 하한
없음”으로 읽힌다. 이 훅은 logtb_expand_trantable에 연결되어 있어 배열이
initialize를 다시 부르지 않고도 성장에 맞춰 추적된다.
불변식 (per-tran-array sized to live tran count).
m_transaction_lowest_visible_mvccids_size == logtb_get_number_of_total_tran_indices ()가 배열 사용 시점에서 항상 성립한다. 강제 수단: 테이블이 커질 가능성이 있는 시점마다 이 가드. 위반 시 (배열이 너무 작으면):build_mvcc_info/complete_mvcc가[tdes.tran_index]를 인덱싱할 때 OOB write가 발생한다.build_mvcc_info는assert (tdes.tran_index < logtb_get_number_of_total_tran_indices ())로 방어한다.
구조체: mvcctable — 모든 필드
섹션 제목: “구조체: mvcctable — 모든 필드”| Field | Role | Why it exists |
|---|---|---|
m_transaction_lowest_visible_mvccids | tran index당 atomic<MVCCID>. 가장 오래된 visible MVCCID | 전역 oldest-visible의 원천. 동적 축 |
m_transaction_lowest_visible_mvccids_size | 그 배열의 할당된 길이 | alloc_transaction_lowest_active의 리사이즈 감지 |
m_current_status_lowest_active_mvccid (atomic) | 현재 status의 가장 낮은 활성 MVCCID | 빠른 워터마크. 스냅샷의 시드 |
m_current_trans_status | 살아 있는 단일 변경 가능한 status (mutex 하) | 쓰기 대상. ring으로 발행됨 |
m_trans_status_history_position (atomic) | 가장 최근 발행된 ring 슬롯의 인덱스 | lock-free reader의 진입점 |
m_trans_status_history | 발행된 read-only 스냅샷 2048 슬롯의 ring | writer를 블록하지 않고도 안정된 과거 status 제공 |
m_new_mvccid_lock (mutex) | log_Gl.hdr.mvcc_next_id의 read-and-forward 보호 | MVCCID 발급 직렬화 |
m_active_trans_mutex (mutex) | 현재 status 변경 + ring 발행 보호 | 한 번에 하나의 완료만 status를 변경 |
m_oldest_visible (atomic) | VACUUM이 쓰는 캐시된 전역 oldest-visible | 쿼리마다 다시 스캔하는 비용을 회피 |
m_ov_lock_count (atomic) | m_oldest_visible을 고정한 holder 수 | VACUUM이 워터마크를 동결 (Chapter 9) |
2.6 부팅/재시작 시드: reset_start_mvccid
섹션 제목: “2.6 부팅/재시작 시드: reset_start_mvccid”initialize는 마커를 MVCCID_FIRST로 시드하지만, 실제 부팅이나 재시작 시점에는
이미 MVCCID가 발급된 상태이므로 마커를 persisted 카운터로부터 다시 시드해야
한다.
// mvcctable::reset_start_mvccid -- src/transaction/mvcc_table.cppvoid mvcctable::reset_start_mvccid () // not thread safe (header comment){ m_current_trans_status.m_active_mvccs.reset_start_mvccid (log_Gl.hdr.mvcc_next_id); assert (m_trans_status_history_position < HISTORY_MAX_SIZE); m_trans_status_history[m_trans_status_history_position] .m_active_mvccs.reset_start_mvccid (log_Gl.hdr.mvcc_next_id); /* <- only the CURRENT ring slot */ m_current_status_lowest_active_mvccid.store (log_Gl.hdr.mvcc_next_id);}// mvcc_active_tran::reset_start_mvccid -- src/transaction/mvcc_active_tran.cppvoid mvcc_active_tran::reset_start_mvccid (MVCCID mvccid){ m_bit_area_start_mvccid = mvccid; if (m_initialized) { check_valid (); } }세 가지를 복구된 값으로 다시 가리키게 한다 — 현재 status의 active-set,
현재 발행된 ring 슬롯의 active-set, 그리고 전역 lowest-active.
m_trans_status_history_position에 있는 슬롯만 건드린다 — 재시작 후 살아 있는
것은 그것뿐이며 나머지 2047개는 stale-but-zeroed 상태로 있다가 ring이
전진하면서 다시 발행된다. log_Gl.hdr.mvcc_next_id를 전진시키는 유일한 코드는
get_new_mvccid/get_two_new_mvccid다 (m_new_mvccid_lock 하의
MVCCID_FORWARD). reset_start_mvccid는 log_initialize_internal과
log_recovery.c의 세 지점에서 (analysis 후, redo 후, finish 시) 실행되며,
모두 헤더가 다시 만들어진 직후다. “not thread safe”는 이 시점들이 동시 트랜잭션
이전이므로 문제가 되지 않는다.
stateDiagram-v2 [*] --> Constructed : mvcctable ctor \n markers MVCCID_FIRST, arrays NULL Constructed --> Initialized : initialize \n allocs ring + per-tran array, seeds MVCCID_FIRST Initialized --> Reseeded : reset_start_mvccid \n markers to log_Gl.hdr.mvcc_next_id Reseeded --> Reseeded : recovery re-runs reset_start_mvccid Reseeded --> Serving : recovery done, accept transactions Serving --> Finalized : finalize \n free ring, free per-tran array Finalized --> [*]
Figure 2-3. mvcctable 생애주기 — initialize는 한 번, reset_start_mvccid는
복구 단계마다 한 번씩, alloc_transaction_lowest_active는 리사이즈마다 다시
실행된다.
2.7 챕터 요약 — 핵심 정리
섹션 제목: “2.7 챕터 요약 — 핵심 정리”- MVCCID 카운터는 테이블에 없다 —
log_Gl.hdr.mvcc_next_id에 살며,m_new_mvccid_lock하의get_new_mvccid/get_two_new_mvccid만 전진시킨다. 테이블은reset_start_mvccid로 재동기화되는 파생 마커만 보관한다. - 두 가지 크기 축 —
m_long_tran_mvccids와m_transaction_lowest_visible_mvccids는 동적 (트랜잭션 인덱스 수에 맞춰) 이고, 비트 영역 (500 워드 = 32000 비트)과 ring (2048)은 정적이다. overflow는 이전 (비트 영역에서 long-tran으로) 또는 wrap (ring)으로 흡수한다. ALL_ACTIVE == 0이 zero-init을 의미 있게 만든다 — 모든new[]()가 이미 “모두 활성, 길이 0”을 뜻하므로initialize가 비트를 따로 비울 필요가 없고, trailing-words-clear 불변식이 공짜로 성립한다.- 살아있는 테이블은 비트 영역을 2049개 들고 있다 — 현재 status + 2048 ring
슬롯. 단순한
new[2048]은 생성자만 실행하므로 슬롯별initialize가 힙 작업을 마무리한다. initialize/reset/reset_active_transactions/finalize는 서로 다르다 — 할당, 살아 있는 prefix만 0으로 + 시작 마커를MVCCID_NULL로, 전체 버퍼를 0으로 (failed-copy 재시도용), 해제 및 비초기화.alloc_transaction_lowest_active만이 재실행 가능한 할당이다 — 크기 변경 가드가total_tran_indices가 다를 때만 재할당한다 (슬롯은MVCCID_NULL로 값-초기화).logtb_expand_trantable에 연결되어 있다.reset_start_mvccid가 부팅/재시작 솔기이다 — 현재 status, 현재 ring 슬롯, 전역 lowest-active를 복구된 카운터로 다시 가리키게 한다. 단일 스레드 복구 전용이다.
Chapter 3: MVCCID 탄생과 레코드 내 헤더
섹션 제목: “Chapter 3: MVCCID 탄생과 레코드 내 헤더”이 챕터는 다음 질문에 답한다 — 버전이 MVCCID를 언제 획득하는가, 그리고 그
도장이 힙 레코드에 어떻게 직렬화되어, delete나 prev-version 메타데이터를 쓸
일이 없던 레코드는 추가 바이트를 한 푼도 쓰지 않게 되는가? 같은 분야의
상위 문서(cubrid-mvcc.md의 MVCCID 할당 정책 / 레코드별 헤더)는
CUBRID가 왜 lazy 발급과 flag 기반 헤더를 채택했는지를 설명한다. 여기서는 모든
분기를 추적한다. 생애주기는 탄생 (쓰기 트랜잭션당 한 번, spinlock 하에서
지연 발급되는 64비트 MVCCID)과 직렬화 (그 MVCCID와 선택적인 delete-id,
prev-version 포인터가 rep 워드의 상위 바이트에 있는 5비트 플래그 필드로 그
길이를 결정하는 가변 길이 헤더로 인코딩되는 과정)의 두 단계로 갈라진다.
3.1 탄생: curr_mvcc_info->id를 통한 지연 발급
섹션 제목: “3.1 탄생: curr_mvcc_info->id를 통한 지연 발급”읽기 전용 트랜잭션은 절대 쓰지 않으므로 MVCCID가 필요 없다. 트랜잭션
디스크립터(LOG_TDES)에는 mvccinfo.id가 들어 있으며 첫 쓰기가 도장을
요구할 때까지 MVCCID_NULL (= 0)로 남아 있다. 게이트는
logtb_get_current_mvccid다.
// logtb_get_current_mvccid -- src/transaction/log_tran_table.cif (MVCCID_IS_VALID (curr_mvcc_info->id) == false) /* <- mint UNCONDITIONALLY, first write only */ curr_mvcc_info->id = log_Gl.mvcc_table.get_new_mvccid ();if (!tdes->mvccinfo.sub_ids.empty ()) return tdes->mvccinfo.sub_ids.back (); /* <- sub shadows parent; parent already minted */return curr_mvcc_info->id;순서가 핵심이다 — 발급/유효성 검사가 무조건 먼저 실행되므로, sub-transaction이
열려 있더라도 부모의 id가 (혹은 그 유효성이) 먼저 확인된 뒤에야 sub_ids가
sub-id를 반환한다. 분기는 (1) id가 유효함 → 할당 없음, 같은 MVCCID가 모든
행에 쓰임 (원자적 가시성), (2) sub_ids가 비어 있지 않음 → sub-id (Chapter 10),
(3) 그 외에는 부모 id를 반환한다.
불변식 — 쓰기 트랜잭션당 도장은 하나.
curr_mvcc_info->id는 정확히 한 번, 오직 쓰기에서만MVCCID_NULL에서<정상 id>로 전이된다.MVCCID_IS_VALID가드가 이를 강제하며, 트랜잭션 종료 시(logtb_complete_mvcc/reset())에만 다시MVCCID_NULL로 돌아간다. 트랜잭션 도중 재발급되면 같은 트랜잭션의 두 행이 서로 다른 도장을 가지게 되어 reader가 찢어진 쓰기를 볼 수 있다.
크래시 복구를 위한 비-lazy 진입점도 존재한다. logtb_rv_assign_mvccid_for_undo_recovery는
tdes->mvccinfo.id = mvccid를 log 레코드에서 그대로 강제 설정한다 — 복구
시점에는 id가 이미 알려져 있어 그대로 복원할 뿐, 재발급하지 않는다. “유효”와
“all-visible”의 계약은 storage_common.h에서 온다 — MVCCID_NULL은 0,
MVCCID_ALL_VISIBLE은 리터럴 3, MVCCID_FIRST는 4. 따라서 정상 id는 항상
>= 4다. 이 예약된 낮은 값들 덕분에 헤더가 “insert id 없음”을 “모두에게
보임”으로 오버로드할 수 있다 (§3.5).
flowchart TD
A["현재 MVCCID 필요"] --> D{"id 유효?"}
D -- "yes" --> S{"sub_ids 비어 있나?"}
D -- "no, 첫 쓰기" --> F["get_new_mvccid(): id = mvcc_next_id; FORWARD"]
F --> S
S -- "no" --> C["return sub_ids.back()"]
S -- "yes" --> E["return curr_mvcc_info->id"]
Figure 3-1: logtb_get_current_mvccid의 지연 발급. 발급/유효성 검사가
sub_ids 검사보다 먼저 실행된다 — 소스 순서와 일치한다.
3.2 할당기: get_new_mvccid와 get_two_new_mvccid
섹션 제목: “3.2 할당기: get_new_mvccid와 get_two_new_mvccid”카운터는 전역 log 헤더 (log_Gl.hdr.mvcc_next_id)에 있으며, 짧은 임계 구역을
가진 전용 락으로 증가시킨다.
// mvcctable::get_new_mvccid -- src/transaction/mvcc_table.cppm_new_mvccid_lock.lock ();id = log_Gl.hdr.mvcc_next_id;MVCCID_FORWARD (log_Gl.hdr.mvcc_next_id); /* <- ++, skipping reserved 0..3 */m_new_mvccid_lock.unlock ();MVCCID_FORWARD는 wrap-guard가 붙은 (id)++다 — 증가 후 MVCCID_FIRST (4)보다
작아지면 (64비트 unsigned wrap에서만 발생) 다시 4로 되돌아간다. 따라서 0..3은
실제로는 절대 발급되지 않는다.
불변식 — 단조 증가하며 gap을 허용하는 발급.
m_new_mvccid_lock하에서 호출자마다 읽고 전진시키므로 모든 id는 직전 id보다 엄밀히 크다. gap은 예상되고 무해하다 — 트랜잭션이 id를 발급받고 롤백할 수 있다. 가시성은 순서에 의존하지 contiguity에 의존하지 않는다. 락을 풀면 두 스레드가 같은 값을 읽고 도장을 공유하게 되어 §3.1이 깨진다.
get_two_new_mvccid는 트랜잭션의 첫 쓰기가 sub-transaction 안에서 일어나는
parent+sub 케이스를 다룬다. 한 번의 락 획득 안에서 두 번 전진시키며
(first는 parent에, second는 sub에 할당). 유일한 호출자인
logtb_get_new_subtransaction_mvccid는 부모가 이미 유효한 id를 가지면
하나만 (get_new_mvccid) 발급한다. 부모 id가 여전히 MVCCID_NULL일 때만
get_two_new_mvccid를 호출한다. 따라서 부모는 항상 쌍 중 작은 id를 받게
되며 — 부모는 active-set에서 자신의 sub-transaction 앞에 정렬되어야 한다
(Chapter 10).
3.3 구조체: mvcc_rec_header
섹션 제목: “3.3 구조체: mvcc_rec_header”or_mvcc_get_header가 레코드를 MVCC_REC_HEADER로 역직렬화한다. 메모리 표현은
디스크 형식보다 넓다 — 모든 필드가 RAM에는 항상 존재하고, 플래그가 설정된
필드만 다시 기록된다.
// mvcc_rec_header -- src/transaction/mvcc.hstruct mvcc_rec_header { INT32 mvcc_flag:8; /* MVCC flags */ INT32 repid:24; /* representation id */ int chn; /* cache coherency number */ MVCCID mvcc_ins_id; /* MVCC insert id */ MVCCID mvcc_del_id; /* MVCC delete id */ LOG_LSA prev_version_lsa; /* log address of previous version */};| 필드 | 역할 | 존재 이유 |
|---|---|---|
mvcc_flag:8 | 하위 5비트 (OR_MVCC_FLAG_MASK = 0x1f)가 어떤 선택 멤버가 디스크에 있는지 결정 | 플래그가 가변 길이 인코딩의 스키마 그 자체다. 헤더 길이와 §3.4의 모든 분기를 결정한다 |
repid:24 | 행 스키마 버전의 representation id | 동일 32비트 워드에 패킹 (상위 바이트는 플래그, 하위 24비트는 repid). 한 번의 OR_GET_INT로 둘 다 복원 |
chn | 캐시 일관성 번호. 비-MVCC 갱신 시 증가. delete-id와 슬롯 공유 — VALID_DELID 클리어 (살아있음) → 슬롯은 실제 CHN, in-RAM의 mvcc_del_id는 MVCCID_NULL. 셋이면 → 슬롯은 무효이고, 실제 deleter MVCCID는 8바이트 뒤 | 의미상 레코드는 CHN을 갖거나 DELID를 갖거나, 둘 중 하나일 뿐 동시에 둘 다는 아니다 — §3.4의 VALID_DELID 불변식 참고 |
mvcc_ins_id | inserting 트랜잭션의 MVCCID | 탄생 도장. reader의 스냅샷과 비교하여 insert 가시성을 결정 |
mvcc_del_id | deleting/updating 트랜잭션의 MVCCID | delete나 in-place update에만 존재. 일반 케이스에서 부재는 8바이트 절약 |
prev_version_lsa | 이전 버전의 log LSA | 버전 체인을 뒤로 추적. 갱신된 행에만 존재 |
classDiagram
class mvcc_rec_header {
+INT32 mvcc_flag : 8
+INT32 repid : 24
+int chn
+MVCCID mvcc_ins_id
+MVCCID mvcc_del_id
+LOG_LSA prev_version_lsa
}
class OR_MVCC_FLAGS {
VALID_INSID 0x01
VALID_DELID 0x02
VALID_PREV_VERSION 0x04
MASK 0x1f
}
mvcc_rec_header --> OR_MVCC_FLAGS : 하위 5비트가 디스크 필드를 선택
Figure 3-2: 메모리 헤더와 그것의 디스크 투영을 결정하는 플래그.
3.4 디스크 레이아웃과 offset 산술
섹션 제목: “3.4 디스크 레이아웃과 offset 산술”첫 4바이트 워드 (OR_REP_OFFSET, OR_MVCC_REP_SIZE = 4)는 하위 24비트에 repid를,
상위 바이트에 플래그를 패킹한다 (OR_MVCC_FLAG_SHIFT_BITS = 24로 시프트). CHN
워드 (OR_CHN_SIZE = 4)는 항상 그다음에 온다. 그 뒤는 모두 조건부이며
누적적이다 — 각 선택 offset은 앞의 모든 존재하는 필드의 크기를 더한다.
// offset macros -- src/base/object_representation.h#define OR_MVCC_INSERT_ID_OFFSET (OR_CHN_OFFSET + OR_CHN_SIZE) /* = 8 */#define OR_MVCC_DELETE_ID_OFFSET(f) \ (OR_MVCC_INSERT_ID_OFFSET + (((f) & OR_MVCC_FLAG_VALID_INSID) ? OR_MVCC_INSERT_ID_SIZE : 0))#define OR_MVCC_PREV_VERSION_LSA_OFFSET(f) \ (OR_MVCC_DELETE_ID_OFFSET(f) + (((f) & OR_MVCC_FLAG_VALID_DELID) ? OR_MVCC_DELETE_ID_SIZE : 0))VALID_INSID만 가진 레코드는 delete-id 자리에 아무것도 두지 않는다. 플래그
→ 길이 룩업 테이블은 mvcc_header_size_lookup[8]이다 (활성 3비트로 인덱싱).
플래그 000 -> 8, 001/010 -> 16, 011 -> 24, … 111 -> 32로,
id 플래그마다 OR_MVCCID_SIZE를 더하고 prev-version 비트에는
OR_MVCC_PREV_VERSION_LSA_SIZE를 더한다. 양 끝은 각각
OR_MVCC_MIN_HEADER_SIZE = 8 (선택 필드 없음)과 OR_MVCC_MAX_HEADER_SIZE = 32
(세 개 모두 존재)다. 한 번도 갱신된 적 없는 살아있는 행은 16바이트 헤더를
갖는다 (rep + CHN + insid). delete-id와 prev-version 슬롯은 페이지에 아예
존재하지 않는다. 이것이 “사용하지 않는 슬롯이 0바이트를 차지한다”는 성질이며,
조건부 offset 매크로와 크기 테이블에서 자연스럽게 도출되는 결과다.
불변식 — 플래그 비트와 물리적 길이는 일치한다.
mvcc_header_size_lookup[flag]는or_mvcc_set_*시퀀스가 쓰는 바이트 수와 같아야 한다.or_mvcc_set_header가 이것을 강제한다 —old_mvcc_size와new_mvcc_size를 비교하고 다르면HEAP_MOVE_INSIDE_RECORD로 슬롯 영역을 늘리거나 줄여서 페이로드 바이트가 덮어쓰이지 않게 한다. 플래그가 꺼진 필드를 setter가 쓴다면 (혹은 그 반대도), 이후의 모든 offset이 어긋나 본문이 garbage로 파싱된다.
3.4.1 or_mvcc_get_header — 분기 단위 추적
섹션 제목: “3.4.1 or_mvcc_get_header — 분기 단위 추적”역직렬화는 고정된 필드 순서로 진행되며, 각 단계마다 플래그를 확인한다 —
repid와 mvcc_flag는 rep 워드에서 풀어내고, 그다음 or_mvcc_get_chn (항상),
or_mvcc_get_insid, or_mvcc_get_delid, or_mvcc_get_prev_version_lsa (각각
플래그 게이트). 각 호출 뒤에는 if (rc != NO_ERROR) goto exit_on_error;가
있고, exit_on_error는 코드를 반환하며 실패 시 er_errid() / ER_FAILED로
폴백한다. 헬퍼들은 하나의 지배 성질을 공유한다 — 플래그가 꺼져 있어 건너뛴
getter는 sentinel을 반환하고 buf->ptr을 그대로 둔다. 그래야 다음 읽기가
정렬을 유지하고, 누적 offset이 매크로를 호출하지 않고도 자체 일관성을 유지한다.
sentinel만 필드별로 다르다.
-
or_mvcc_get_insid: 플래그 클리어 →MVCCID_ALL_VISIBLE반환. set → BIGINT 읽고OR_MVCCID_SIZE만큼 전진.// or_mvcc_get_insid -- src/base/object_representation_sr.cif (!(mvcc_flags & OR_MVCC_FLAG_VALID_INSID))return MVCCID_ALL_VISIBLE; /* <- ptr NOT advanced */// ... reads BIGINT, buf->ptr += OR_MVCCID_SIZE ... -
or_mvcc_get_delid: 클리어 →MVCCID_NULL반환. set → 읽고-전진. -
or_mvcc_get_chn: 무조건 — CHN은 플래그가 없다 (슬롯이 항상 존재). 즉OR_INT_SIZE만큼 읽고 전진한다. 슬롯이 CHN인지 delete id인지는VALID_DELID가 결정한다 (§3.3). -
or_mvcc_get_prev_version_lsa: 클리어 →LSA_SET_NULL. set → 8바이트를 구조체 할당으로 복사하고 전진.
3.4.2 or_mvcc_set_header, or_mvcc_add_header, setter들
섹션 제목: “3.4.2 or_mvcc_set_header, or_mvcc_add_header, setter들”or_mvcc_set_header는 기존 헤더를 다시 쓴다 (위에서 본 resize-aware 경로).
or_mvcc_add_header는 새 레코드 앞에 프리펜드한다 (record->length == 0을
assert하고, 쓴 바이트 수로 record->length를 설정). 둘 다 같은 시퀀스를
실행한다 — set_repid_and_flags -> set_chn -> set_insid -> set_delid -> set_prev_version_lsa — 각각 플래그에서 short-circuit 한다. or_mvcc_set_insid는
VALID_INSID가 클리어면 아무것도 쓰지 않고 NO_ERROR를 반환하며, 그렇지 않으면
or_put_bigint를 호출한다. or_mvcc_set_delid와 or_mvcc_set_prev_version_lsa도
같은 식이다. or_mvcc_set_chn은 무조건적이다. 유일한 구조적 차이는 set은 먼저
HEAP_MOVE_INSIDE_RECORD를 호출해 크기를 맞추고, add는 길이 0 레코드에 append
한다는 점뿐이다.
or_mvcc_get_flag / or_mvcc_set_flag는 플래그 바이트만 바꿔야 할 때 쓰는
좁은 접근자다.
// or_mvcc_set_flag -- src/base/object_representation_sr.crepid_and_flag = OR_GET_INT (record->data + OR_REP_OFFSET);repid_and_flag &= ~OR_MVCC_FLAG_MASK; /* <- clears LOW 5 bits 0x1f */repid_and_flag += ((flags & OR_MVCC_FLAG_MASK) << OR_MVCC_FLAG_SHIFT_BITS); /* <- ADD into bits 24+ */// ... or_put_int writes it back at OR_REP_OFFSET ...특이한 점 — 마스크의 대상 (하위 5비트)이 플래그가 결합되는 곳 (24비트 이상)이
아니며, 결합 연산은 비트 OR가 아니라 +=다. 그래도 동작하는 이유는, 워드에
마지막으로 쓴 코드가 이미 상위 플래그 영역을 비워두었기 때문이다.
or_mvcc_set_flag는 레코드 크기를 조정하지 않으므로, 호출자는 새 플래그에
맞추어 물리적 레이아웃을 따로 맞춰야 한다 — set_header와 비교해 미끄러운
지점이다.
flowchart TD
A["or_mvcc_set_header(record, hdr)"] --> B["old=lookup[old_flag]\nnew=lookup[hdr.flag]"]
B --> C{"old != new?"}
C -- "yes" --> D{"area_size big enough?"}
D -- "no" --> E["assert(false); exit_on_error"]
D -- "yes" --> F["HEAP_MOVE_INSIDE_RECORD"]
C -- "no" --> G["set repid+flags"]
F --> G
G --> H["set_chn -> set_insid -> set_delid -> set_prev_version_lsa"]
H --> I["NO_ERROR"]
Figure 3-3: or_mvcc_set_header의 resize-aware 헤더 재작성.
3.5 힙 계층 진입점과 도장이 놓이는 자리
섹션 제목: “3.5 힙 계층 진입점과 도장이 놓이는 자리”heap_get_mvcc_header는 context->record_type으로 분기한다. REC_HOME →
spage_get_record(..., PEEK) 후 or_mvcc_get_header. PEEK 실패와 get 오류는
둘 다 assert(false) 후 S_ERROR를 반환 (구성상 불가능 — 페이지는 latch되어
있고 슬롯은 검증된 상태). REC_BIGONE → forward 페이지가 필요하므로 overflow
reader에 위임. REC_RELOCATION → context->forward_oid.slotid의 forward
페이지를 읽고 같은 get + 오류 처리. default → assert(false), S_ERROR —
다른 유형은 호출자 버그다.
heap_get_mvcc_rec_header_from_overflow는 특수 경우다. overflow 레코드는 항상
최대 크기 헤더를 저장한다. 호출자가 peek_recdes로 NULL을 넘기면, 스택의
ovf_recdes 스크래치 버퍼로 폴백한 뒤 ->data를
overflow_get_first_page_data (ovf_page)로 가리키게 하고 ->length를
OR_MVCC_MAX_HEADER_SIZE로 강제한 다음 or_mvcc_get_header를 호출한다 —
overflow 페이지에서는 선택 필드가 항상 실체화되어 있기 때문이다 (자매 함수
heap_set_mvcc_rec_header_on_overflow가 VALID_INSID/VALID_DELID를 강제 설정
하고 OR_MVCC_MAX_HEADER_SIZE를 assert한다). 따라서 0바이트 슬롯 최적화는
home/relocation에서만 성립하는 성질이다 — 큰 레코드는 고정 레이아웃을 위해
공간을 양보한다.
insert 도장은 어디서 기록될까? 새로 만들어진 레코드는 VALID_INSID를
가지며, 이는 heap_attrinfo_transform_header_to_disk (와
heap_insert_adjust_recdes_header)에서
repid_bits |= (OR_MVCC_FLAG_VALID_INSID << OR_MVCC_FLAG_SHIFT_BITS)로 세팅된다.
하지만 insid 슬롯은 초기에는 0이다. 실제 MVCCID는 log 시점에
logtb_get_current_mvccid(thread_p)로 얻는다. heap_mvcc_log_insert는 §3.1의
지연 발급을 강제하고, rep 워드, CHN, 본문만 redo crumb로 기록한다 — insid
바이트는 절대로. 분기는 레코드 크기와 logging 모드에 따라 달라진다. REC_BIGONE이
아닌 레코드는 네 개의 redo crumb를 보낸다 (레코드 타입, rep 워드 OR_INT_SIZE,
CHN OR_INT_SIZE, 그다음 OR_HEADER_SIZE(p_recdes->data)부터의 본문 —
insid 슬롯 너머). REC_BIGONE인 경우 헤더 crumb를 건너뛰고 레코드 타입과
전체 레코드 본문만 기록한다 (overflow 페이지가 자체 max-size 헤더를 들고
있다). thread_p->no_logging이 설정되어 있으면
log_append_undo_crumbs (RVHF_MVCC_INSERT, ...)를 redo 없이 호출하고,
그렇지 않으면 log_append_undoredo_crumbs를 호출한다.
// heap_mvcc_log_insert -- src/storage/heap_file.credo_crumbs[n_redo_crumbs].length = sizeof (p_recdes->type); /* <- always: record type */redo_crumbs[n_redo_crumbs++].data = &p_recdes->type;if (p_recdes->type != REC_BIGONE) { // ... rep-word OR_INT_SIZE, CHN OR_INT_SIZE crumbs ... data_copy_offset = OR_HEADER_SIZE (p_recdes->data); /* <- body starts past insid slot */ } /* <- REC_BIGONE: data_copy_offset stays 0 */redo_crumbs[n_redo_crumbs].length = p_recdes->length - data_copy_offset;redo_crumbs[n_redo_crumbs++].data = p_recdes->data + data_copy_offset;if (thread_p->no_logging) log_append_undo_crumbs (thread_p, RVHF_MVCC_INSERT, p_addr, 0, NULL); /* <- undo only */else log_append_undoredo_crumbs (thread_p, RVHF_MVCC_INSERT, p_addr, 0, n_redo_crumbs, NULL, redo_crumbs);redo 시에는 heap_rv_mvcc_redo_insert가 MVCC_SET_INSID (&mvcc_rec_header, rcv->mvcc_id)로 다시 도장을 찍고 or_mvcc_add_header를 호출한다. 따라서 MVCCID는
논리적으로 lazy하게 발급되고 (§3.1), log 레코드의 mvcc_id를 통해 흐르며,
물리적으로는 redo/apply 경로에서 insid 슬롯에 기록된다 — 복구는 rcv->mvcc_id에서
도장을 다시 유도하므로 진실의 출처가 두 군데로 갈라지지 않는다.
3.6 플래그 해석: MVCC_IS_HEADER_* 매크로
섹션 제목: “3.6 플래그 해석: MVCC_IS_HEADER_* 매크로”헤더가 RAM에 들어오면 호출자는 세 개의 boolean 질문을 한다. 각각 플래그 테스트와 값 테스트를 결합한다.
// MVCC_IS_HEADER_* -- src/transaction/mvcc.h#define MVCC_IS_HEADER_DELID_VALID(h) \ (MVCC_IS_FLAG_SET (h, OR_MVCC_FLAG_VALID_DELID) && MVCCID_IS_VALID (MVCC_GET_DELID (h)))#define MVCC_IS_HEADER_INSID_NOT_ALL_VISIBLE(h) \ (MVCC_IS_FLAG_SET (h, OR_MVCC_FLAG_VALID_INSID) && MVCC_GET_INSID (h) != MVCCID_ALL_VISIBLE)#define MVCC_IS_HEADER_ALL_VISIBLE(h) \ (!MVCC_IS_FLAG_SET (h, OR_MVCC_FLAG_VALID_INSID|OR_MVCC_FLAG_VALID_DELID) \ && MVCC_GET_INSID (h) == MVCCID_ALL_VISIBLE)이중 검사는 절반만 만들어진 헤더를 막는다 — 플래그는 설정됐지만 id는 아직
MVCCID_NULL인 (생성 도중인) 경우거나, 메모리 구조체가 플래그는 없는데
MVCCID_ALL_VISIBLE을 가지고 있는 (정확히 or_mvcc_get_insid가 VALID_INSID
클리어 시 반환하는 값) 경우가 가능하다. ALL_VISIBLE은 VACUUM이 insid 플래그를
벗겨낼 만큼 오래된 행의 정상 상태다 — insid 없음, delid 없음, in-RAM insid가
literal 3으로 읽힘 — 따라서 스냅샷 비교 없이 무조건 보이게 된다. 이 매크로들은
Chapter 6 (가시성)과 Chapter 7 (VACUUM 술어)에 공급된다.
3.7 미해결 질문 — 여분의 두 플래그 비트
섹션 제목: “3.7 미해결 질문 — 여분의 두 플래그 비트”OR_MVCC_FLAG_MASK는 다섯 비트 (0x1f)를 예약하는데, 정의된 것은 셋뿐이다 —
VALID_INSID (0x01), VALID_DELID (0x02), VALID_PREV_VERSION (0x04). 비트
0x08과 0x10은 마스크와 시프트는 받지만 어떤 의미도 부여되지 않으며,
mvcc_header_size_lookup은 [8] 크기다 — 정의된 세 비트만 인덱싱하므로 0x08을
설정하면 OOB 인덱스가 된다. 헤더 주석은 의도적 예약이라 시사하지만 의도된
용도는 문서화되어 있지 않다. 네 번째 on-record MVCC 필드를 추가하려는 사람은
mvcc_header_size_lookup을 넓히고, 모든 누적 offset 매크로를 확장하고,
overflow 페이지의 OR_MVCC_MAX_HEADER_SIZE = 32 단언을 감사해야 한다.
3.8 챕터 요약 — 핵심 정리
섹션 제목: “3.8 챕터 요약 — 핵심 정리”- MVCCID는 쓰기 트랜잭션당 한 번, 지연 발급된다.
logtb_get_current_mvccid는curr_mvcc_info->id가MVCCID_NULL일 때만 발급한다. 발급/유효성 테스트가 sub_ids 분기보다 무조건 먼저 실행되므로 sub-transaction 경로조차도 부모 id를 먼저 발급한다. - 할당기는 작은 락-가드 카운터다.
get_new_mvccid는log_Gl.hdr.mvcc_next_id를m_new_mvccid_lock하에서 읽고MVCCID_FORWARD한다.get_two_new_mvccid는 parent+sub 부트스트랩을 위해 두 번 전진시켜 부모에게 더 작은 id를 준다. 롤백으로 인한 gap은 무해하다. mvcc_rec_header는 디스크보다 메모리에서 더 넓다.mvcc_flag의 하위 5비트가 insid, delid, prev_version_lsa 중 어느 것이 물리적으로 존재하는지 결정하며,chn슬롯은 delete-id 영역과 역할을 공유한다.- 0바이트 슬롯은 누적 offset 매크로와
mvcc_header_size_lookup[8]에서 온다. 갱신되지 않은 살아있는 행은 16바이트 헤더를 갖는다. 범위는OR_MVCC_MIN_HEADER_SIZE = 8과OR_MVCC_MAX_HEADER_SIZE = 32다. - Get/set 헬퍼는 플래그 게이트이며 포인터 일관성을 유지한다. 필드를
건너뛴 getter는 포인터를 전진시키면 안 된다.
or_mvcc_set_header는HEAP_MOVE_INSIDE_RECORD로 크기를 맞추고,or_mvcc_set_flag는 하위 5비트를 비우고 새 플래그를 비트 24+ 자리에+=로 결합한다 — 리사이즈는 안 한다, 미끄러운 부분이다. - 도장은 복구/적용 경로에서 기록된다. insert는
heap_attrinfo_transform_header_to_disk/heap_insert_adjust_recdes_header를 통해VALID_INSID플래그가 켜지고 insid는 0인 헤더를 만든다.heap_mvcc_log_insert가 발급을 강제하고,heap_rv_mvcc_redo_insert의MVCC_SET_INSID(..., rcv->mvcc_id)에서 실제 MVCCID가 자리잡는다. overflow 레코드는 항상 max-size 헤더를 저장한다. - 다섯 플래그 비트 중 둘은 사용되지 않으며
[8]길이 크기 테이블로는 도달조차 불가능하다 — 이후 on-record MVCC 필드를 추가하려는 사람에게는 제약이다.
Chapter 4: Active-Set 조회 — 비트 영역 탐색과 캐시된 스칼라
섹션 제목: “Chapter 4: Active-Set 조회 — 비트 영역 탐색과 캐시된 스칼라”Chapter 1–3은 active-set의 표현 (슬라이딩 m_bit_area, overflow
m_long_tran_mvccids, on-record 헤더)을 만들었다. 이 챕터의 질문은 두
가지다 — MVCCID 하나가 주어졌을 때 코드는 그것이 아직 활성인지 어떻게
판단하는가? 그리고 두 개의 캐시된 short-circuit 스칼라
(lowest_active_mvccid, highest_completed_mvccid)는 원시 비트로부터 어떻게
도출되는가?
세 계층이 쌓여 있다 — mvcc_active_tran::is_active의 비트 영역 탐색,
비트 영역을 스칼라로 평탄화하는 도출 스캔
compute_highest_completed_mvccid / compute_lowest_active_mvccid,
그리고 스칼라를 먼저 시도하고 활성 윈도 안에서만 탐색으로 폴백하는 래퍼
mvcc_is_id_in_snapshot / mvcc_is_active_id. 왜 short-circuit이 빠른
경로인지는 cubrid-mvcc.md의 스냅샷 가시성 항목을 참고하라. 이 챕터는
코드를 추적한다.
4.1 등장하는 세 구조체
섹션 제목: “4.1 등장하는 세 구조체”탐색은 mvcc_active_tran에, 스칼라는 mvcc_snapshot에, 트랜잭션별 캐시는
mvcc_info에 산다.
flowchart TB INFO["mvcc_info (LOG_TDES에 들어 있는, 활성 tran별)\nid / recent_snapshot_lowest_active_mvccid / sub_ids"] SNAPSHOT["mvcc_snapshot\nlowest_active_mvccid / highest_completed_mvccid (스칼라)"] ACTIVE["mvcc_active_tran\nm_bit_area + start + length / m_long_tran_mvccids"] INFO -->|snapshot 소유| SNAPSHOT SNAPSHOT -->|m_active_mvccs 소유| ACTIVE
Figure 4-1. mvcc_info는 mvcc_snapshot을 소유하고, mvcc_snapshot은
mvcc_active_tran 비트 영역과 두 개의 파생 스칼라를 소유한다.
mvcc_active_tran — Chapter 1에서 매핑됨. 읽기 경로 필드만 다시 정리한다.
| Field | Role | Why it exists |
|---|---|---|
m_bit_area | unit_type[BITAREA_MAX_SIZE]. 비트 i set ⇒ start+i 커밋됨 | 최근에 완료된 MVCCID의 밀집 윈도 |
m_bit_area_start_mvccid | 비트 offset 0에 매핑된 MVCCID | 탐색이 이 값을 빼서 비트 offset을 만든다 |
m_bit_area_length | 윈도 길이 (비트 단위) | 범위 검사. 0이면 모두 활성 |
m_long_tran_mvccids | 윈도 아래의 활성 MVCCID 오름차순 배열 | 윈도가 밀려난 long-runner를 보관 |
m_long_tran_mvccids_length | 위 배열의 개수 | 루프 한계. [0]은 전역 lowest active |
m_initialized | 할당 가드 | 읽기 경로는 m_bit_area != NULL을 가정 |
mvcc_snapshot — 호출자가 보는 스냅샷 레코드.
| Field | Role | Why it exists |
|---|---|---|
lowest_active_mvccid | 스칼라. 이 값보다 엄격히 작은 id는 커밋됨 | 첫 번째 short-circuit. PRECEDES면 스냅샷에 없음 |
highest_completed_mvccid | 스칼라. 이 값 이상은 이 스냅샷에 대해 활성 | 두 번째 short-circuit. FOLLOW_OR_EQUAL이면 스냅샷 안 |
m_active_mvccs | 빌드 시점에 복사된 mvcc_active_tran 비트 영역 | 활성 윈도 안의 권위적 답 |
snapshot_fnc | 스냅샷에 묶인 가시성 함수 | build_mvcc_info가 설정. Ch. 5–6 |
valid | 스냅샷이 채워져 있는가 | stale 읽기 가드 |
mvcc_info — 모든 활성 트랜잭션의 LOG_TDES에 붙어 있다.
| Field | Role | Why it exists |
|---|---|---|
snapshot | 트랜잭션 자신의 mvcc_snapshot | 문장당 또는 트랜잭션당 한 번 빌드 |
id | 자신의 MVCCID (쓰지 않았으면 MVCCID_NULL) | logtb_is_current_mvccid가 매칭 — 내 쓰기는 나에게 활성 |
recent_snapshot_lowest_active_mvccid | 가장 최근 스냅샷의 lowest-active | mvcc_is_active_id short-circuit. 이 아래면 커밋됨, 테이블 건너뜀 |
sub_ids | 실행 중인 sub-transaction MVCCID들 | logtb_is_current_mvccid가 함께 매칭 (Ch. 10) |
is_sub_active | sub-transaction 실행 중 | sub-transaction 부기 (Ch. 10) |
불변식 (비트 의미론). set 비트는 커밋됨, clear 비트는 활성을 뜻한다 — 자연스러운 읽기의 반대다. 따라서
is_active는!is_set(position)을 반환한다.ALL_ACTIVE = 0과ALL_COMMITTED = (unit_type) -1이 극단의 워드 패턴이다. 이 극성을 뒤집으면 모든 가시성 결정이 뒤집혀 커밋된 행이 사라진다.
4.2 비트 주소 지정 — 네 개의 헬퍼
섹션 제목: “4.2 비트 주소 지정 — 네 개의 헬퍼”모든 탐색은 “어느 워드, 어느 비트”로 귀결된다. 짧지만 핵심을 떠받치는 네 개의 인라인 헬퍼가 산술을 처리한다 — 1만큼만 어긋나도 윈도 전체의 가시성이 손상된다.
// get_bit_offset / get_unit_of / get_mask_of / is_set -- src/transaction/mvcc_active_tran.cppsize_t get_bit_offset (MVCCID mvccid) const { return static_cast<size_t> (mvccid - m_bit_area_start_mvccid); } /* <- MVCCID to bit index */unit_type *get_unit_of (size_t bit_offset) const { return m_bit_area + (bit_offset / UNIT_BIT_COUNT); } /* <- which 64-bit word (UNIT_BIT_COUNT==64) */static unit_type get_mask_of (size_t bit_offset) { return ((unit_type) 1) << (bit_offset & 0x3F); } /* <- bit within word; &0x3F == mod 64 */bool is_set (size_t bit_offset) const { return ((*get_unit_of (bit_offset)) & get_mask_of (bit_offset)) != 0; }get_unit_of는 64로 나누어 워드를 찾고, get_mask_of의 & 0x3F는 워드 내
비트를 위한 빠른 % 64이며, is_set이 둘을 합친다. is_set은 범위 검사가
없다 — 호출자가 먼저 offset이 m_bit_area_length 안에 있는지 확인해야 한다
(§4.3 불변식).
4.3 is_active — 세 경우의 탐색
섹션 제목: “4.3 is_active — 세 경우의 탐색”스택의 바닥이다 — “이 MVCCID가 이 캡처된 active-set에서 활성인가?”를 상호 배타적인 세 경우로 답한다.
// mvcc_active_tran::is_active -- src/transaction/mvcc_active_tran.cpp if (MVCC_ID_PRECEDES (mvccid, m_bit_area_start_mvccid)) /* CASE 1: below the window */ { if (m_long_tran_mvccids != NULL) for (size_t i = 0; i < m_long_tran_mvccids_length; i++) if (mvccid == m_long_tran_mvccids[i]) return true; /* <- in long-tran overflow: active */ return false; /* <- below window, not long-tran: committed */ } else if (m_bit_area_length == 0) /* CASE 2: empty window */ return true; /* <- nothing committed yet: active */ else /* CASE 3: inside / above the window */ { size_t position = get_bit_offset (mvccid); if (position < m_bit_area_length) return !is_set (position); /* <- in window: set bit == committed */ else return true; /* <- above highest tracked bit: active */ }MVCC_ID_PRECEDES(id1, id2)는 (id1) < (id2)다. 위의 CASE 1/2/3 주석이 각
분기를 설명한다.
불변식 (탐색 범위 안전).
is_set/get_unit_of는 CASE 3의position < m_bit_area_length테스트를 통과한 뒤에만 dereference된다. 그 CASE 3에 도달하려면m_bit_area_length != 0이어야 한다. 분기 순서가 핵심적이다 — CASE 2를 CASE 3 뒤로 옮기면 길이 0 윈도가is_set까지 도달한다. “m_bit_area_length너머의 모든 비트는ALL_ACTIVE다” 라는 불변식 (check_valid, Ch. 2)이 범위 밖 경우를 활성으로 답해도 안전하게 만든다.
4.4 compute_highest_completed_mvccid — 최상위 set 비트 (top-down)
섹션 제목: “4.4 compute_highest_completed_mvccid — 최상위 set 비트 (top-down)”비트 영역을 가장 큰 완료된 MVCCID로 평탄화한다. build_mvcc_info (Ch. 5)가
결과를 MVCCID_FORWARD로 한 칸 더 밀어 올린다.
// mvcc_active_tran::compute_highest_completed_mvccid -- src/transaction/mvcc_active_tran.cpp if (m_bit_area_length == 0) return m_bit_area_start_mvccid - 1; /* <- EMPTY: nothing completed; one below start */ // ... declarations condensed ... for (highest_completed_bit_area = get_unit_of (m_bit_area_length - 1); /* <- scan units top-down */ highest_completed_bit_area >= m_bit_area; --highest_completed_bit_area) { bits = *highest_completed_bit_area; if (bits == 0) continue; /* <- ALL_ACTIVE unit: keep going down */ for (bit_pos = 0, count_bits = UNIT_BIT_COUNT / 2; count_bits > 0; count_bits /= 2) if (bits >= (1ULL << count_bits)) /* <- in-word search for highest set bit */ { bit_pos += count_bits; bits >>= count_bits; } highest_bit_position = bit_pos; break; } if (highest_completed_bit_area < m_bit_area) /* <- ran off bottom: no set bit anywhere */ return m_bit_area_start_mvccid - 1; else return get_mvccid (units_to_bits (highest_completed_bit_area - m_bit_area) + highest_bit_position);빈 윈도와 찾지 못한 경우 모두 m_bit_area_start_mvccid - 1을 반환한다. 찾은
경로는 ALL_ACTIVE 워드를 건너뛰며 top-down으로 워드를 스캔하고, 워드 안에서는
6단계 (log2 64) 이진 탐색으로 최상위 set 비트를 찾는다 (software clz).
4.5 compute_lowest_active_mvccid — 최하위 clear 비트 (bottom-up)
섹션 제목: “4.5 compute_lowest_active_mvccid — 최하위 clear 비트 (bottom-up)”거울 — 가장 작은 여전히 활성인 MVCCID. complete_mvcc (Ch. 8)가 워터마크를
전진시킬 때 호출한다.
// mvcc_active_tran::compute_lowest_active_mvccid -- src/transaction/mvcc_active_tran.cpp if (m_long_tran_mvccids_length > 0 && m_long_tran_mvccids != NULL) return m_long_tran_mvccids[0]; /* <- SHORTCUT: sorted, [0] is lowest active */ if (m_bit_area_length == 0) return m_bit_area_start_mvccid; /* <- EMPTY: lowest active is window start */ unit_type *end_bit_area = get_unit_of (m_bit_area_length - 1); size_t lowest_bit_pos = 0; // other declarations condensed for (lowest_active_bit_area = m_bit_area; lowest_active_bit_area <= end_bit_area; ++lowest_active_bit_area) { bits = *lowest_active_bit_area; if (bits == ALL_COMMITTED) /* <- whole word committed: skip 64 bits */ { lowest_bit_pos += UNIT_BIT_COUNT; continue; } for (bit_pos = 0, count_bits = UNIT_BIT_COUNT / 2; count_bits > 0; count_bits /= 2) { mask = (1ULL << count_bits) - 1; if ((bits & mask) == mask) /* <- low count_bits all set: clear bit is higher */ { bit_pos += count_bits; bits >>= count_bits; } } lowest_bit_pos += bit_pos; break; } if (lowest_active_bit_area > end_bit_area) /* <- every tracked bit set: no active bit */ return get_mvccid (m_bit_area_length); else return get_mvccid (lowest_bit_pos);조기 return들이 정렬된 배열의 [0] 지름길과 빈 윈도를 처리한다. 그 외에는
워드를 위 방향으로 스캔하면서 (ALL_COMMITTED는 +64로 건너뜀) 내부 루프가
§4.4의 거울로 최하위 clear 비트를 찾는다. 찾지 못하면
get_mvccid(m_bit_area_length)를 반환한다.
불변식 (정렬된 overflow 배열).
[0]지름길이 옳은 것은 오직m_long_tran_mvccids가 오름차순이기 때문이다 —add_long_transaction은 새 항목이 이전 마지막 값보다 크다는 것을 assert한다. 순서가 깨지면 워터마크가 앞으로 점프하고 VACUUM이 여전히 보이는 버전을 회수할 수 있다.
4.6 스냅샷 계층 래퍼 — mvcc_is_id_in_snapshot
섹션 제목: “4.6 스냅샷 계층 래퍼 — mvcc_is_id_in_snapshot”두 스칼라를 탐색 앞에 둬서 활성 윈도 안에서만 스캔이 실행되게 한다.
// mvcc_is_id_in_snapshot -- src/transaction/mvcc.c (signature/assert elided) if (MVCC_ID_PRECEDES (mvcc_id, snapshot->lowest_active_mvccid)) return false; /* <- below lowest active: committed before snapshot */ if (MVCC_ID_FOLLOW_OR_EQUAL (mvcc_id, snapshot->highest_completed_mvccid)) return true; /* <- at/above highest completed: active wrt snapshot */ return snapshot->m_active_mvccs.is_active (mvcc_id); /* <- gray zone: consult the bit area */회색 영역만 §4.3 탐색에 도달한다. (MVCC_ID_FOLLOW_OR_EQUAL은 >=이고,
build_mvcc_info는 `highest_completed_mvccid = compute_highest_completed_mvccid()
- 1
을 설정한다 — Ch. 5의MVCCID_FORWARD`.)
4.7 스냅샷 계층 래퍼 — mvcc_is_active_id
섹션 제목: “4.7 스냅샷 계층 래퍼 — mvcc_is_active_id”“이 MVCCID가 지금 활성인가?”에 답한다 (동결된 스냅샷이 아니라. dirty/delete 경로는 Ch. 7에서 다룬다).
// mvcc_is_active_id -- src/transaction/mvcc.c (tdes lookup / asserts elided) curr_mvcc_info = &tdes->mvccinfo; if (MVCC_ID_PRECEDES (mvccid, curr_mvcc_info->recent_snapshot_lowest_active_mvccid)) return false; /* <- below recent lowest-active: committed */ if (logtb_is_current_mvccid (thread_p, mvccid)) return true; /* <- my own id or a sub-id: active to me */ return log_Gl.mvcc_table.is_active (mvccid); /* <- otherwise ask the shared table */두 short-circuit 모두 락 없이 답한다 — recent_snapshot_lowest_active_mvccid
아래면 (커밋됨), logtb_is_current_mvccid가 자신의 id/sub_ids와 매칭하면
(자기 자신에게 활성). 그 외에는 mvcctable::is_active를 참조한다 (§4.8).
4.8 테이블 계층 mvcctable::is_active — 버전 검증 재시도
섹션 제목: “4.8 테이블 계층 mvcctable::is_active — 버전 검증 재시도”공유 active-set은 lock-free history ring (m_trans_status_history, Ch. 8)에
산다. 커밋 중인 트랜잭션이 스캔 도중에 살아있는 항목을 바꿔 끼울 수 있으므로,
is_active는 탐색 전후로 m_version을 검증한다.
// mvcctable::is_active -- src/transaction/mvcc_table.cpp (decls elided) do { index = m_trans_status_history_position.load (); /* <- current ring slot */ version = m_trans_status_history[index].m_version.load ();/* <- snapshot the version BEFORE */ ret_active = m_trans_status_history[index].m_active_mvccs.is_active (mvccid); /* <- §4.3 probe */ } while (version != m_trans_status_history[index].m_version.load ()); /* <- version moved? redo */ return ret_active;불변식 (버전 안정성). 읽기는 탐색 전후로
m_version이 일치할 때만 신뢰된다. writer는 증가된m_version으로 새 status 슬롯을 publish한 뒤m_trans_status_history_position을 그쪽으로 전진시킨다 (next_trans_status_start가 다음 슬롯에 증가된 버전을 찍고,next_tran_status_finish가 슬롯이 완성된 뒤 position을 전진시킨다 — 둘 다 Ch. 8). swap 전에 index와 version을 잡은 reader는 다시 로드할 때 mismatch를 보고 재시도한다. 재시도 제한은 없다 — 진행은 writer가 reader보다 짧다는 사실에 의존한다.
4.9 챕터 요약 — 핵심 정리
섹션 제목: “4.9 챕터 요약 — 핵심 정리”- set 비트는 커밋, clear 비트는 활성을 의미한다 (자연스러운 읽기의 반대).
is_active는!is_set(position)을 반환하며, 극단의 워드는ALL_ACTIVE = 0/ALL_COMMITTED = -1이다. is_active는 순서가 정해진 세 경우다 — 윈도 아래 (정렬된 long-tran 배열 스캔, 아니면 커밋), 빈 윈도 (활성), 윈도 안/위 (비트 룩업, 또는 추적된 길이 위는 활성). 이 순서가is_set이 빈/범위 밖 윈도에서 dereference되는 것을 막는다.- 두 도출 스캔은 서로의 거울이다 — top-down으로 최상위 set 비트를 찾고,
bottom-up으로 최하위 clear 비트를 찾는다. 둘 다 6단계 워드 내 이진 탐색과
명시적 fallback (
start - 1,get_mvccid(m_bit_area_length))을 갖는다. compute_lowest_active_mvccid는m_long_tran_mvccids[0]로 short-circuit 한다 — overflow 배열이 오름차순으로 유지되기 때문이다.- 캐시된 스칼라가 탐색을 앞지른다.
mvcc_is_id_in_snapshot은 두 스칼라 사이의 회색 영역에서만 비트 영역을 건드린다.mvcc_is_active_id는recent_snapshot_lowest_active_mvccid캐시와logtb_is_current_mvccid(자신의 id + sub-ids)를 더한다. - 공유 테이블 읽기는 락 없이 낙관적이다 —
mvcctable::is_active가 탐색 전후로m_version을 검증하며 변경 시 재시도한다.
Chapter 5: 스냅샷 생성
섹션 제목: “Chapter 5: 스냅샷 생성”읽기 전용 트랜잭션은 전역 커밋 상태를 자신만의 private 구조체로 사진 찍어두고,
시스템을 동결시키는 대신 그 사진을 기준으로 읽는다. 이 챕터는 그 사진이
어떻게 찍히는지를 분석한다 — logtb_get_mvcc_snapshot의 진입 가드,
mvcctable::build_mvcc_info 내부의 lock-free 재시도 루프, VACUUM을 정직하게
유지시키는 엄격한 publish 순서, 그리고 어떤 바이트가 각 스냅샷 필드를
채우는지. 스냅샷의 의미는 동반 문서 cubrid-mvcc.md의 “Snapshot isolation”
절에 있다. Ch. 6은 여기서 만들어진 구조체를 소비하고, Ch. 4는 여기서 복사되는
mvcc_active_tran 레이아웃을 설명한다.
5.1 이 챕터가 채우는 네 구조체
섹션 제목: “5.1 이 챕터가 채우는 네 구조체”스냅샷은 하나의 객체가 아니다. mvcc_snapshot이 mvcc_info 안에 중첩되어
있고, 그것은 하나의 mvcc_trans_status 슬롯 복사본으로 채워지며, 그 페이로드는
mvcc_active_tran 비트 영역이다. Figure 5-1은 그 포함 관계다.
flowchart LR
subgraph TDES["log_tdes (트랜잭션마다)"]
INFO["mvcc_info mvccinfo"]
end
INFO --> SNAP["mvcc_snapshot snapshot"]
SNAP --> ACT["mvcc_active_tran m_active_mvccs"]
subgraph GLOBAL["mvcctable (전역)"]
HIST["m_trans_status_history[index]\nmvcc_trans_status"]
end
HIST --> HACT["mvcc_active_tran m_active_mvccs"]
HACT -. "copy_to THREAD_UNSAFE" .-> ACT
Figure 5-1. 스냅샷 생성은 전역 history 슬롯 하나의 비트 영역을 트랜잭션 private 스냅샷으로 복사한다.
mvcc_snapshot (mvcc.h) — 읽기 대상이 되는 사진.
| Field | Role | Why it exists |
|---|---|---|
lowest_active_mvccid | 이 값보다 <이면 커밋됨. 비트 탐색 불필요 | 가시성 동안의 빠른 하한 컷오프 (Ch. 6) |
highest_completed_mvccid | 이 값 >=이면 스냅샷 이후 탄생, 즉 보이지 않음 | 빠른 상한 컷오프 (Ch. 6) |
m_active_mvccs | 두 경계 사이의 MVCCID별 커밋/활성 상태 비트 영역 | 모호한 중간 범위의 정확한 답 |
snapshot_fnc | 가시성 술어에 대한 함수 포인터. mvcc_satisfies_snapshot으로 설정 | 호출자가 가시성을 다형적으로 호출 (dirty/snapshot 변종이 호출 형태를 공유) |
valid | 완성되면 true. 진입 가드가 확인 | 트랜잭션 도중 재구축 회피 (RR/SR), RC 무효화 신호 |
mvcc_info (mvcc.h) — 트랜잭션별 MVCC 봉투.
| Field | Role | Why it exists |
|---|---|---|
snapshot | 여기서 만든 mvcc_snapshot | 읽기 사진 |
id | 이 트랜잭션의 MVCCID (Ch. 3). 첫 쓰기 전까지는 MVCCID_NULL | 자기 가시성 확인 |
recent_snapshot_lowest_active_mvccid | 스냅샷의 lowest active 캐시 사본 | 스냅샷 구조체 바깥에서 사용되는 두 번째 빠른 컷오프 (형제 술어, Ch. 7) |
sub_ids | sub-transaction MVCCID 스택 (Ch. 10) | savepoint / 중첩 문장 가시성 |
is_sub_active | sub-transaction 실행 중 true | 특수 경로 라우팅 (Ch. 10) |
mvcc_trans_status (mvcc_table.hpp) — 하나의 전역 커밋-상태 ring 슬롯.
| Field | Role | Why it exists |
|---|---|---|
m_active_mvccs | 이 슬롯이 publish된 시점의 권위적 live 비트 영역 | 스냅샷이 복사하는 데이터 |
m_last_completed_mvccid | 슬롯이 쓰일 때 마지막으로 완료된 MVCCID. “just for info” | 디버깅 / 히스토리 포렌식 |
m_event_type | COMMIT/ROLLBACK/SUBTRAN. “just for info” | 디버깅 전용 |
m_version | 슬롯이 다시 쓰일 때마다 증가하는 atomic<version_type> | lock-free 가드 — 복사 전후로 읽는다 |
mvcc_active_tran (mvcc_active_tran.hpp) — 비트 영역 그 자체. 깊은
의미론 (비트 패킹, long-tran 마이그레이션, BITAREA_MAX_SIZE = 500)은 Ch. 4에
있다. 이 챕터는 copy와 reset만 사용한다 (§5.4). 여섯 필드 모두.
| Field | Role | Why it exists |
|---|---|---|
m_bit_area | unit_type[] 비트 버퍼 포인터. 비트 n = start + n의 상태 | 패킹된 커밋/활성 맵 (Ch. 4) |
m_bit_area_start_mvccid | 비트 0이 매핑하는 MVCCID | 비트 범위를 절대 MVCCID에 고정 |
m_bit_area_length | 살아있는 길이 (비트 단위). 너머 비트는 모두 활성 (0) | 유효 prefix 범위. get_bit_area_memsize를 구동 |
m_long_tran_mvccids | start보다 오래된 여전히 활성인 MVCCID overflow 배열 | 비트 영역 윈도에서 밀려난 long transaction |
m_long_tran_mvccids_length | overflow 배열의 항목 수 | long-tran 복사/스캔의 범위 |
m_initialized | 버퍼가 할당되면 true | copy_to/check_valid가 버퍼 접근 전 assert |
5.2 진입 가드 — 누가 스냅샷을 받을 자격이 있는가
섹션 제목: “5.2 진입 가드 — 누가 스냅샷을 받을 자격이 있는가”모든 읽기 경로는 logtb_get_mvcc_snapshot으로 흘러든다. 이것은 builder가
아니라 가드다 — 빌드를 시도할지 말지를 결정한다.
// logtb_get_mvcc_snapshot -- src/transaction/log_tran_table.cLOG_TDES *tdes = LOG_FIND_TDES (LOG_FIND_THREAD_TRAN_INDEX (thread_p));if (!tdes->is_active_worker_transaction ()) { return NULL; /* <- system trans read latest committed, no MVCC photo */ }assert (tdes != NULL); /* <- in source: AFTER the early return */THREAD_ENTRY *main_thread_p = NULL;if (thread_p->m_px_orig_thread_entry != NULL) { main_thread_p = thread_get_main_thread (thread_p); pthread_mutex_lock (&main_thread_p->m_px_lock_mutex); /* <- parallel-px workers share one snapshot */ }if (!tdes->mvccinfo.snapshot.valid) { log_Gl.mvcc_table.build_mvcc_info (*tdes); /* <- only build when invalid */ }if (main_thread_p != NULL) { pthread_mutex_unlock (&main_thread_p->m_px_lock_mutex); }return &tdes->mvccinfo.snapshot;분기 정리:
- 활성 worker 트랜잭션이 아님 →
NULL반환. 시스템 트랜잭션 (VACUUM, checkpoint, 복구)은 MVCC 사진이 없다. 호출자는NULL을 “모든 커밋된 것을 본다”로 다룬다. - parallel-px worker (
m_px_orig_thread_entry != NULL) → main 스레드의m_px_lock_mutex를 획득. px sub-thread는 main 트랜잭션의tdes를 공유 하므로, 락이valid검사와 빌드를 직렬화해야 한다 — 두 worker가 한tdes에 대해 동시에build_mvcc_info를 부르면 안 된다. - 스냅샷이 유효함 → 빌드 건너뛰기 (RR/SR의 첫 문장 이후 흔한 경우). 무효 → 빌드 후, 2단계에서 락을 잡았다면 해제. 항상 포인터를 반환.
불변식 — 유효성 epoch당 한 번의 빌드. build_mvcc_info는 snapshot.valid == false일 때만 실행된다. RR/SR은 트랜잭션 내내 유효한 상태를 유지한다 (한 번
빌드). RC는 logtb_invalidate_snapshot_data가 문장마다 valid = false로 만든다
(§5.6). valid == true인데 다시 빌드하면 스캔 중인 살아있는 스냅샷을 덮어쓰고
진행 중인 가시성 결정을 손상시킨다.
5.3 build_mvcc_info: 버전 재확인을 사용하는 lock-free 복사
섹션 제목: “5.3 build_mvcc_info: 버전 재확인을 사용하는 lock-free 복사”mvcctable::build_mvcc_info는 커밋 경로가 동시에 다시 쓰고 있을지도 모르는
전역 ring 슬롯을 락 없이 복사해야 한다. 이를 낙관적 버전 재확인으로 처리한다.
Figure 5-2가 권위적 제어 흐름이다. 아래 노트는 다이어그램이 보여줄 수 없는
것만 덧붙인다.
flowchart TD
A["스냅샷 비트 영역 초기화"] --> B["tx_lowest_active =\nload m_transaction_lowest_visible[tran_index]"]
B --> C{"MVCCID_IS_VALID\ntx_lowest_active?"}
C -- "no = 아직 publish 안 됨" --> D["내 슬롯 = MVCCID_ALL_VISIBLE\n다음에 crt_status_lowest_active 읽기\n그 다음 다시 저장 -- 엄격한 순서"]
C -- "yes = 이미 publish됨" --> E["crt_status_lowest_active =\nload m_current_status_lowest_active"]
D --> F["index = history_position.load"]
E --> F
F --> G["ver = slot.m_version.load"]
G --> H["slot.m_active_mvccs.copy_to\ndest, THREAD_UNSAFE"]
H --> I["logtb_load_global_statistics_to_tran"]
I --> J{"ver == slot.m_version.load?"}
J -- "yes = 안정" --> K["break"]
J -- "no = writer가 끼어듦" --> L["dest.reset_active_transactions"]
L --> M["retry_count++"]
M --> G
K --> N["check_valid; 스칼라 필드 채움"]
Figure 5-2. build_mvcc_info 주 제어 흐름. 치명적이지 않은 statistics-load 오류 경로 (노드 I)는 본문에서 설명한다 — 오류를 설정하고 루프를 깨지 않는다.
lowest-visible publish dance — 문서화된 VACUUM 경쟁. 이 함수의 가장 미묘한
코드다. !MVCCID_IS_VALID (tx_lowest_active) (아직 publish된 lowest-visible
값이 없음)일 때, 세 개의 atomic을 엄격한 순서로 실행한다 — 내 슬롯에
sentinel MVCCID_ALL_VISIBLE publish, 전역 lowest active 읽기, 그 전역 값을
다시 내 슬롯에 저장.
// mvcctable::build_mvcc_info -- src/transaction/mvcc_table.cppoldest_active_set (m_transaction_lowest_visible_mvccids[tdes.tran_index], tdes.tran_index, MVCCID_ALL_VISIBLE, oldest_active_event::BUILD_MVCC_INFO);/* Is important that between next two code lines to not have delays (e.g. instructions adding). */crt_status_lowest_active = oldest_active_get (m_current_status_lowest_active_mvccid, 0, oldest_active_event::BUILD_MVCC_INFO);oldest_active_set (m_transaction_lowest_visible_mvccids[tdes.tran_index], tdes.tran_index, crt_status_lowest_active, oldest_active_event::BUILD_MVCC_INFO);소스 주석은 sentinel이 깨뜨리는 5단계 시나리오를 보여준다. 핵심 단계를 원문 그대로 인용한다.
- the transaction having global lowest active MVCCID commits, so the global value is updated (advanced)
- the VACUUM thread computes the MVCCID threshold as the updated global lowest active MVCCID
- the snapshot thread resumes and p_transaction_lowest_active_mvccid is set to initial value of global lowest active MVCCID
- the VACUUM thread computes the threshold again and found a value (initial global lowest active MVCCID) less than the previous threshold
즉 VACUUM이 전진된 전역 lowest로부터 threshold를 계산한 뒤, 중단되었던
스냅샷 스레드가 깨어나 자기의 오래된 초기값을 저장하면, VACUUM의 다음
threshold는 이전 threshold보다 작게 계산된다 — 워터마크가 뒤로 가는
것이다. sentinel을 설정하면 compute_oldest_visible_mvccid (Ch. 9)가 이
슬롯을 기다리도록 한다. tx_lowest_active가 이미 유효한 (흔한 재시도
경로) 경우 dance는 건너뛰고 단일 m_current_status_lowest_active_mvccid
로드만 실행한다.
history 슬롯 스냅샷. m_trans_status_history_position은 항상 현재
(최신) 슬롯을 가리킨다 — 커밋 경로가 이를 전진시킨다 (Ch. 8). index를 한 번
읽고, 그 슬롯의 m_version을 복사 전에 읽는다.
// mvcctable::build_mvcc_info -- src/transaction/mvcc_table.cppindex = m_trans_status_history_position.load ();assert (index < HISTORY_MAX_SIZE);const mvcc_trans_status &trans_status = m_trans_status_history[index];trans_status_version = trans_status.m_version.load (); /* <- version BEFORE copy */trans_status.m_active_mvccs.copy_to (tdes.mvccinfo.snapshot.m_active_mvccs, mvcc_active_tran::copy_safety::THREAD_UNSAFE);THREAD_UNSAFE는 의도적이다 — check_valid는 복사 도중에 변경될 수 있는
슬롯에서 실행될 수 없다. 유효성은 루프가 안정성을 확인한 뒤에만 검증된다.
logtb_load_global_statistics_to_tran이 다음에 실행되며, 오류 시
ER_MVCC_CANT_GET_SNAPSHOT을 설정하지만 빌드를 중단하거나 루프를 깨지
않는다.
버전 재확인 — lock-free의 축.
// mvcctable::build_mvcc_info -- src/transaction/mvcc_table.cppif (trans_status_version == trans_status.m_version.load ()) /* <- version AFTER copy */ { break; /* <- writer did not touch slot; copy is consistent */ }else { tdes.mvccinfo.snapshot.m_active_mvccs.reset_active_transactions (); /* <- discard torn copy */ }불변식 — 버전 안정 복사. 호출자에게 전달되는 비트 영역은 정확히 하나의
publish된 mvcc_trans_status 이미지와 같다 — 같은 m_version이 copy_to
전후로 관찰되기 때문이다. 커밋 경로는 모든 슬롯 재작성마다 m_version을
올린다 (Ch. 8). 따라서 동시 쓰기는 감지되고 재시도가 강제된다. 감지 시
reset_active_transactions는 destination을 0으로 만들어 다음 시도에서 stale
tail이 결과를 오염시키지 않게 한다. 재확인 없이는 스냅샷이 pre-commit 비트
영역과 post-commit 경계를 섞을 수 있고, 가시성 (Ch. 6)이 경쟁하는 MVCCID에
대해 일관성 없이 답하게 된다.
루프 뒤 스칼라 채움.
// mvcctable::build_mvcc_info -- src/transaction/mvcc_table.cpptdes.mvccinfo.snapshot.m_active_mvccs.check_valid (); /* <- now safe to validate */highest_completed_mvccid = tdes.mvccinfo.snapshot.m_active_mvccs.compute_highest_completed_mvccid ();MVCCID_FORWARD (highest_completed_mvccid); /* <- exclusive upper bound */tdes.mvccinfo.recent_snapshot_lowest_active_mvccid = crt_status_lowest_active;tdes.mvccinfo.snapshot.snapshot_fnc = mvcc_satisfies_snapshot;tdes.mvccinfo.snapshot.lowest_active_mvccid = crt_status_lowest_active;tdes.mvccinfo.snapshot.highest_completed_mvccid = highest_completed_mvccid;tdes.mvccinfo.snapshot.valid = true; /* <- LAST; publishes the snapshot */compute_highest_completed_mvccid는 (Ch. 4) 비어 있으면
m_bit_area_start_mvccid - 1을 반환한다. MVCCID_FORWARD가 이를 배타적
상한으로 한 칸 밀어 올린다. lowest 두 필드는 crt_status_lowest_active를 받는다.
valid = true가 마지막에 설정되어 peer가 절반 채워진 스냅샷을 절대 보지
않게 한다. 성능 카운팅은 그 후 snapshot_retry_count - 1을
PSTAT_LOG_SNAPSHOT_RETRY_COUNTERS에 더한다 (“minus one”이 의무적인 첫 번째
패스를 빼므로 메트릭은 경합이지 work가 아니다). 경과 시간은
PSTAT_LOG_SNAPSHOT_TIME_COUNTERS에 더한다.
5.4 mvcc_active_tran::copy_to 내부
섹션 제목: “5.4 mvcc_active_tran::copy_to 내부”복사는 크기 지정된 memcpy에 shrink-clear와 long-transaction 꼬리, 그리고
안전 플래그 게이트가 더해진 것이다.
// mvcc_active_tran::copy_to -- src/transaction/mvcc_active_tran.cppassert (m_initialized && dest.m_initialized);if (safety == copy_safety::THREAD_SAFE) { check_valid (); /* <- source validated only when safe */ dest.check_valid (); }size_t new_bit_area_memsize = get_bit_area_memsize (); /* <- source live bytes */size_t old_bit_area_memsize = dest.get_bit_area_memsize (); /* <- dest's previous live bytes */char *dest_bit_area = (char *) dest.m_bit_area;if (new_bit_area_memsize > 0) { std::memcpy (dest_bit_area, m_bit_area, new_bit_area_memsize); }if (old_bit_area_memsize > new_bit_area_memsize) { /* <- dest was longer last time; zero the now-unused tail */ std::memset (dest_bit_area + new_bit_area_memsize, 0, old_bit_area_memsize - new_bit_area_memsize); }if (m_long_tran_mvccids_length > 0) { std::memcpy (dest.m_long_tran_mvccids, m_long_tran_mvccids, get_long_tran_memsize ()); }dest.m_bit_area_start_mvccid = m_bit_area_start_mvccid;dest.m_bit_area_length = m_bit_area_length;dest.m_long_tran_mvccids_length = m_long_tran_mvccids_length;if (safety == copy_safety::THREAD_SAFE) { dest.check_valid (); }다섯 가지 분기 — THREAD_SAFE는 복사 전후로 check_valid로 감싼다 (§5.5의
clone 래퍼, 정적인 source 대상). THREAD_UNSAFE는 그것을 건너뛴다
(build_mvcc_info가 live 슬롯에 대해 부르며, 재확인에 의존한다).
new_bit_area_memsize > 0은 깨끗한 빈 시스템에서 복사를 건너뛴다.
old_bit_area_memsize > new_bit_area_memsize은 destination이 더 긴 영역을
들고 있었을 때 남은 꼬리를 0으로 만든다. m_long_tran_mvccids_length > 0은
overflow 배열을 복사한다 (Ch. 4). 그 다음 스칼라 할당이 메타데이터를 거울처럼
반영한다.
불변식 — 꼬리는 항상 모두 활성 (0)이다. check_valid는 디버그 빌드에서
m_bit_area_length 너머의 모든 비트가 ALL_ACTIVE (0)인지, long-tran
MVCCID들이 엄격히 정렬되어 m_bit_area_start_mvccid에 선행하는지 assert한다.
위의 shrink-clear와 reset_active_transactions가 이를 유지한다. stale 커밋
꼬리 비트가 남아 있으면 compute_highest_completed_mvccid가 active-set 바깥의
MVCCID를 보고하여 상한을 손상시킨다.
reset_active_transactions는 찢어진 복사에 쓰는 무뚝뚝한 reset이다.
// mvcc_active_tran::reset_active_transactions -- src/transaction/mvcc_active_tran.cppstd::memset (m_bit_area, 0, BITAREA_MAX_MEMSIZE); /* <- zero the WHOLE max buffer, not just live part */m_bit_area_length = 0;m_long_tran_mvccids_length = 0;전체 BITAREA_MAX_MEMSIZE를 0으로 만들고 두 길이 모두 0으로 떨어뜨린다.
m_bit_area_start_mvccid는 건드리지 않고 check_valid를 호출하지 않는다
(호출자가 재시도 중이므로 아직 일관되지 않다).
5.5 다른 두 개의 copy_to 래퍼
섹션 제목: “5.5 다른 두 개의 copy_to 래퍼”mvcc_snapshot::copy_to와 mvcc_info::copy_to는 hot path가 아니다 — 이미
완성된 스냅샷을 clone한다 (예 parent → sub-transaction, Ch. 10). source가 완성된
변경 없는 로컬이므로 THREAD_SAFE를 쓴다. mvcc_snapshot::copy_to는
dest.m_active_mvccs.initialize ()를 부른 뒤 copy_to (..., THREAD_SAFE)를
호출하고, 그다음 lowest_active_mvccid, highest_completed_mvccid,
snapshot_fnc, valid를 거울처럼 반영한다 — 모든 필드를. 그래서 clone은
재빌드 없이 사용 가능하다. mvcc_info::copy_to는 this->snapshot.copy_to (dest.snapshot)를 호출한 뒤 봉투 필드를 덮어쓴다.
// mvcc_info::copy_to -- src/transaction/mvcc.cdest.id = this->id;dest.recent_snapshot_lowest_active_mvccid = this->recent_snapshot_lowest_active_mvccid;dest.sub_ids = this->sub_ids; /* <- std::vector deep copy */dest.is_sub_active = this->is_sub_active;5.6 격리 수준은 언제만 결정하지 어떻게는 결정하지 않는다
섹션 제목: “5.6 격리 수준은 언제만 결정하지 어떻게는 결정하지 않는다”build_mvcc_info는 격리 수준에 무관하다. 획득 시점은 호출 사이트와
logtb_invalidate_snapshot_data에 있다.
| Isolation | 스냅샷이 잡히는 시점 | 메커니즘 |
|---|---|---|
TRAN_READ_COMMITTED | 문장당 한 번 | logtb_invalidate_snapshot_data가 문장 경계마다 valid = false 설정. 다음 logtb_get_mvcc_snapshot이 재빌드 |
TRAN_REPEATABLE_READ | 트랜잭션당 한 번 | 첫 logtb_get_mvcc_snapshot이 빌드. valid는 true로 유지 (invalidate가 no-op) |
TRAN_SERIALIZABLE | 트랜잭션당 한 번 | 스냅샷 획득은 RR과 동일 |
가드는 logtb_invalidate_snapshot_data다.
// logtb_invalidate_snapshot_data -- src/transaction/log_tran_table.cif (tdes == NULL || tdes->isolation >= TRAN_REPEATABLE_READ) { return NO_ERROR; /* <- RR/SR keep their snapshot across statements */ }if (tdes->mvccinfo.snapshot.valid) { tdes->mvccinfo.snapshot.valid = false; /* <- RC: drop it so next read rebuilds */ logtb_tran_reset_count_optim_state (thread_p); }RC는 자신의 문장들 사이에 커밋된 트랜잭션을 본다. RR/SR은 첫 문장의 사진에
고정된다. >= TRAN_REPEATABLE_READ 테스트는 enum 순서 RC < RR < SR에 의존한다.
어느 것도 build_mvcc_info를 건드리지 않는다.
5.7 챕터 요약 — 핵심 정리
섹션 제목: “5.7 챕터 요약 — 핵심 정리”- 진입 가드가 모든 것을 게이트한다.
logtb_get_mvcc_snapshot은 시스템 트랜잭션에NULL을 반환하고, parallel-px worker를m_px_lock_mutex에서 직렬화하며,valid == false일 때만 빌드한다. - 생성은 버전 재확인을 통한 lock-free다.
build_mvcc_info는m_version을THREAD_UNSAFEcopy_to전후로 읽는다. 같으면 → 일관됨 (break), 다르면 → 찢어진 상태 (reset_active_transactions하고 재시도). - lowest-visible dance가 VACUUM 워터마크 역행을 막는다.
MVCCID_ALL_VISIBLEsentinel을 먼저 쓰고, 전역 lowest를 읽고, 중간 작업 없이 다시 쓴다. 그래야 VACUUM (Ch. 9)이 undercut하지 않고 기다린다. THREAD_UNSAFE와THREAD_SAFE는 source에 관한 것이다. 빌드 경로는 live 변경 가능한 슬롯을 복사한다 (check_valid건너뛰고 재확인에 의존). clone 경로는 정적인 로컬을 복사한다 (검증).- 비트 영역 꼬리는 0으로 유지되어야 한다.
copy_to의 shrink-clear,reset_active_transactions의 전체 버퍼memset,check_valid의 assert가m_bit_area_length너머에 stale 커밋 비트를 남기지 않게 한다. - 스칼라 필드는 고정 순서로 채워지며
valid가 마지막이다.highest_completed = MVCCID_FORWARD(...)가 배타적 상한이다. 두 lowest 필드는 전역 lowest를 받는다.snapshot_fnc = mvcc_satisfies_snapshot이다. - 격리 수준 타이밍은 외부적이다. RC는 문장당 재빌드
(
logtb_invalidate_snapshot_data를 통해). RR/SR은 한 번만 빌드. builder는 격리 수준에 무관하다.
Chapter 6: 가시성 판정
섹션 제목: “Chapter 6: 가시성 판정”Chapter 2–5의 MVCC 장치는 초당 수백만 번 묻는 단 하나의 yes/no 질문에 답하기
위해 존재한다 — 이 트랜잭션이 읽는 스냅샷과 한 레코드 버전의 MVCC 헤더가
주어졌을 때, 이것이 내가 보아야 할 버전인가? 이 챕터는 mvcc_satisfies_snapshot을
분기마다 — 모든 조건, return, perfmon leaf까지 — 해부한다. 개념적 틀
(스냅샷을 반열린 MVCCID 구간으로 보는 관점, “active”의 의미, committed-before
/ active / committed-after 모델)은 동반 문서 cubrid-mvcc.md의 “Snapshot
semantics”와 “Visibility” 절에 있다. 이 챕터는 그것을 다시 유도하지 않는다.
6.1 세 입력과 세 출력
섹션 제목: “6.1 세 입력과 세 출력”mvcc_satisfies_snapshot은 스레드 (나는 누구인가), 레코드 헤더, 스냅샷에
대한 순수 결정 함수다 — 부수 효과가 없다. 스냅샷을 변경하는
mvcc_satisfies_dirty (Ch. 7)와는 다르다. 판정은 세 가지 enum 값 중 하나다.
// mvcc_satisfies_snapshot_result -- src/transaction/mvcc.henum mvcc_satisfies_snapshot_result{ TOO_OLD_FOR_SNAPSHOT, /* not visible, deleted by me or deleted by inactive transaction */ SNAPSHOT_SATISFIED, /* is visible and valid */ TOO_NEW_FOR_SNAPSHOT /* not visible, inserter is still active. * ... check previous versions in log ... */};| 판정 | 의미 | 호출자가 다음에 하는 일 |
|---|---|---|
SNAPSHOT_SATISFIED | 정확히 이 버전이 reader가 보는 것이다 | 레코드 반환 |
TOO_NEW_FOR_SNAPSHOT | 너무 늦게 태어났다. 더 오래된 버전이 보일 수 있다 | prev_version_lsa를 따라 한 칸 뒤로 가서 다시 평가 (Ch. 3) |
TOO_OLD_FOR_SNAPSHOT | reader 입장에서 이미 죽었다. 더 오래된 버전이 살릴 수 없다 | 멈춰라 — reader는 이 체인 head에서 아무것도 보지 않는다 |
방향성 불변식. TOO_NEW는 버전 체인의 뒤쪽을 가리킨다. TOO_OLD는
종착점이다. 스냅샷 안에서 커밋된 delete는 더 오래된 버전으로 되돌릴 수 없다 —
그 버전은 삭제된 같은 논리적 행이다. 따라서 TOO_NEW의 enum 주석만이 “check
previous versions in log”를 언급한다. §6.7에 worked example이 있다.
6.2 등장하는 구조체
섹션 제목: “6.2 등장하는 구조체”mvcc_satisfies_snapshot은 두 구조체를 읽는다 — mvcc_rec_header (버전별
도장)와 mvcc_snapshot (가시성 경계). 둘 다 Chapter 1에서 완전히 정의되었다.
아래 필드 테이블은 모든 멤버를 다룬다.
mvcc_rec_header (src/transaction/mvcc.h) — 필드 mvcc_flag:8, repid:24,
chn, mvcc_ins_id, mvcc_del_id, prev_version_lsa.
| Field | Role here | Why it exists |
|---|---|---|
mvcc_flag | MVCC_IS_HEADER_DELID_VALID와 MVCC_IS_FLAG_SET(.., VALID_INSID)로 읽힘 — 어떤 도장이 있는가 | 버전이 insert 도장을 갖지 않을 수도 (vacuum이 떼어냄) 있고 delete 도장이 없을 수도 (한 번도 삭제 안 됨) 있다 |
repid / chn | 안 읽음 | representation id / 캐시 일관성 번호 — 레코드 형식과 클라이언트 캐시 |
mvcc_ins_id | inserter의 MVCCID. 스냅샷과 “나”에 비교 | 이 버전을 나타나게 만든 트랜잭션 |
mvcc_del_id | deleter의 MVCCID. 스냅샷과 “나”에 비교 | 이 버전을 사라지게 만든 트랜잭션 |
prev_version_lsa | 안에서 안 읽음. TOO_NEW 시 호출자가 따라가는 링크 | 버전 체인을 뒤로 연결 (Ch. 3) |
플래그 비트는 object_representation_constants.h에 있다 —
OR_MVCC_FLAG_VALID_INSID = 0x01, OR_MVCC_FLAG_VALID_DELID = 0x02.
MVCC_IS_HEADER_DELID_VALID(h)는 MVCC_IS_FLAG_SET (h, OR_MVCC_FLAG_VALID_DELID) && MVCCID_IS_VALID (MVCC_GET_DELID (h))다 — 플래그가 설정되어 있고 id가
MVCCID_NULL이 아닌 경우.
mvcc_snapshot (src/transaction/mvcc.h) — 필드 lowest_active_mvccid,
highest_completed_mvccid, m_active_mvccs, snapshot_fnc, valid, 그리고
멤버 함수 (ctor, reset, deleted operator=, copy_to).
| Field | Role here | Why it exists |
|---|---|---|
lowest_active_mvccid | mvcc_is_id_in_snapshot. 엄격히 작은 id는 committed-before (보임) | 의심 구간의 하한. 빠른 reject |
highest_completed_mvccid | mvcc_is_id_in_snapshot. 이상의 id는 스냅샷 시점에 활성 (스냅샷 안) | 상한. 빠른 accept |
m_active_mvccs | 경계 사이의 id에 대한 비트 영역 / 캐시 스칼라 탐색 (Ch. 4) | 동시 id의 정확한 멤버십 테스트 |
snapshot_fnc | 안 읽음 — 이 함수를 선택한 포인터 | 스냅샷 / dirty / delete를 다형적으로 연결 |
valid | 안 읽음 (호출자가 빌드 보장) | 스냅샷이 완성됐음을 표시 (Ch. 5) |
| member functions | 호출 안 함 | 생성 / 복사 헬퍼 (Ch. 5) |
flowchart TD
H["mvcc_rec_header\nmvcc_flag, mvcc_ins_id, mvcc_del_id"]
S["mvcc_snapshot\nlowest_active_mvccid, highest_completed_mvccid, m_active_mvccs"]
F["mvcc_satisfies_snapshot"]
H -->|"DELID_VALID? INSID 플래그? ins_id / del_id"| F
S -->|"is_id_in_snapshot(ins_id / del_id)"| F
TH["thread_p\nlogtb_is_current_mvccid -> me?"]
TH -->|"INSERTED_BY_ME / DELETED_BY_ME"| F
F --> V{"판정"}
V --> R1["SNAPSHOT_SATISFIED"]
V --> R2["TOO_NEW_FOR_SNAPSHOT"]
V --> R3["TOO_OLD_FOR_SNAPSHOT"]
Figure 6-1 — 판정의 입력. 헤더는 도장과 플래그를, 스냅샷은 경계를, 스레드는 정체성을 제공한다.
6.3 상위 분기와 경계 헬퍼
섹션 제목: “6.3 상위 분기와 경계 헬퍼”첫 분기가 이 함수의 유일한 구조적 fork다.
// mvcc_satisfies_snapshot -- src/transaction/mvcc.c assert (rec_header != NULL && snapshot != NULL);
if (!MVCC_IS_HEADER_DELID_VALID (rec_header)) { /* The record is not deleted */ // ... insert-side ladder (§6.4) ... } else { /* The record is deleted */ // ... delete-side ladder (§6.5) ... }버전이 “삭제되지 않음”은 VALID_DELID가 클리어이거나 mvcc_del_id == MVCCID_NULL인 경우다 — 둘 다 MVCC_IS_HEADER_DELID_VALID로 합쳐진다.
삭제되지 않은 쪽은 “inserter가 나에게 보이게 되었는가?”만 묻는다. 삭제된
쪽은 inserter 와 deleter 모두를 추론하므로 ladder가 더 길다. 둘 다
하나의 MVCCID에 대한 구간 테스트인 mvcc_is_id_in_snapshot에 의존한다.
// mvcc_is_id_in_snapshot -- src/transaction/mvcc.c (body, condensed) if (MVCC_ID_PRECEDES (mvcc_id, snapshot->lowest_active_mvccid)) return false; /* below band -> committed-before, NOT in-snapshot */ if (MVCC_ID_FOLLOW_OR_EQUAL (mvcc_id, snapshot->highest_completed_mvccid)) return true; /* at/above band -> not completed, IS in-snapshot */ return snapshot->m_active_mvccs.is_active (mvcc_id); /* inside band -> exact probe (Ch. 4) */“in snapshot”이 의미하는 것은 이 MVCCID가 스냅샷이 잡힌 순간에 여전히
활성이었거나 아직 시작하지 않았음이다 — 그 효과는 보이지 않아야 한다. 매크로
MVCC_IS_REC_INSERTER_IN_SNAPSHOT / MVCC_IS_REC_DELETER_IN_SNAPSHOT은
mvcc_ins_id / mvcc_del_id를 이 헬퍼에 넣어주는 얇은 래퍼다.
6.4 not-deleted ladder — 네 가지 경우
섹션 제목: “6.4 not-deleted ladder — 네 가지 경우”// mvcc_satisfies_snapshot -- src/transaction/mvcc.c (not-deleted branch, condensed) if (!MVCC_IS_HEADER_DELID_VALID (rec_header)) { if (!MVCC_IS_FLAG_SET (rec_header, OR_MVCC_FLAG_VALID_INSID)) { /* ... perfmon ... */ return SNAPSHOT_SATISFIED; } /* (a) no insert stamp -> all-visible */ else if (MVCC_IS_REC_INSERTED_BY_ME (thread_p, rec_header)) { /* ... perfmon ... */ return SNAPSHOT_SATISFIED; } /* (b) I inserted it */ else if (MVCC_IS_REC_INSERTER_IN_SNAPSHOT (thread_p, rec_header, snapshot)) { /* ... perfmon ... */ return TOO_NEW_FOR_SNAPSHOT; } /* (c) inserter still in-snapshot */ else { /* ... perfmon ... */ return SNAPSHOT_SATISFIED; } /* (d) inserter committed before snapshot */ }Figure 6-2는 네 개의 short-circuit 경우를 부호화한다. (a) 떼어진 insert 도장은
vacuum이 이미 이 버전을 all-visible로 선언했다는 뜻이다. (b)
MVCC_IS_REC_INSERTED_BY_ME는 logtb_is_current_mvccid에 도달하며, 트랜잭션
자신의 sub-transaction id도 매칭한다 (Ch. 10). (c)는 유일한 TOO_NEW 경로다.
(d)는 소거법으로 남는 잔여 — sub-classification이 있는 유일한 leaf다 (§6.8).
insert-side 불변식. not-deleted 분기에서 판정이 TOO_NEW인 것은 오직
inserter가 동시일 때뿐이다 (case c). 그 외에는 SNAPSHOT_SATISFIED다.
무조건 보이는 테스트 (a, b)를 in-snapshot 테스트 (c)보다 먼저 놓아 강제한다 —
정체성이 경계보다 먼저다. 스냅샷 이전에 커밋된 inserter에 대해 (c)가 발화하면
reader는 이전 버전을 쫓아가 stale row를 노출시킨다. 정확성은 구간 안의
mvcc_is_id_in_snapshot이 정확하다는 데에 달려 있다 (Ch. 4).
flowchart TD
A{"VALID_INSID 플래그 설정?"}
A -->|"no"| AV["SNAPSHOT_SATISFIED\nINSERTED_VACUUMED / VISIBLE"]
A -->|"yes"| B{"INSERTED_BY_ME?"}
B -->|"yes"| BV["SNAPSHOT_SATISFIED\nINSERTED_CURR_TRAN / VISIBLE"]
B -->|"no"| C{"inserter IN_SNAPSHOT?"}
C -->|"yes"| CV["TOO_NEW_FOR_SNAPSHOT\nINSERTED_OTHER_TRAN / INVISIBLE"]
C -->|"no"| DV["SNAPSHOT_SATISFIED\nINSERTED_COMMITED[_LOST] / VISIBLE"]
Figure 6-2 — not-deleted ladder. in-snapshot inserter만 TOO_NEW를 낳는다.
6.5 deleted ladder — 네 가지 경우
섹션 제목: “6.5 deleted ladder — 네 가지 경우”MVCC_IS_HEADER_DELID_VALID가 true이면 버전이 커밋되었거나 보류 중인 delete
도장을 들고 있으므로 inserter와 deleter 모두가 중요하다.
// mvcc_satisfies_snapshot -- src/transaction/mvcc.c (deleted branch, condensed) else { if (MVCC_IS_REC_DELETED_BY_ME (thread_p, rec_header)) { /* ... perfmon ... */ return TOO_OLD_FOR_SNAPSHOT; } /* (e) I deleted it */ else if (MVCC_IS_REC_INSERTER_IN_SNAPSHOT (thread_p, rec_header, snapshot)) { /* !!TODO: Is this check necessary? It seems that if inserter is active, then so will be the deleter (actually * they will be the same). It only adds an extra-check in a function frequently called. */ /* ... perfmon ... */ return TOO_NEW_FOR_SNAPSHOT; /* (f) inserter in-snapshot */ } else if (MVCC_IS_REC_DELETER_IN_SNAPSHOT (thread_p, rec_header, snapshot)) { /* ... perfmon ... */ return SNAPSHOT_SATISFIED; } /* (g) deleter in-snapshot -> not yet visible */ else { /* ... perfmon ... */ return TOO_OLD_FOR_SNAPSHOT; } /* (h) deleter committed before snapshot */ }Figure 6-3가 경우들을 부호화한다 — (e) 종착점. 거슬러 올라가면 내 delete를
되살린다. (f) 동시 트랜잭션이 insert한 뒤 커밋 전에 delete까지 했다. 그래서
이 버전은 reader 입장에서는 존재한 적이 없다 (TOO_NEW). (g) 유일한 보이는
deleted leaf — inserter는 committed-before지만 deleter가 아직 동시이므로
행은 미삭제처럼 보인다. (h) 그 외 — 두 도장 모두 committed-before (TOO_OLD,
종착). (f)의 !!TODO는 §6.10에서 다룬다.
delete-side 불변식. deleted 분기에서 SNAPSHOT_SATISFIED는 오직 (g)에서만
(deleter가 동시). TOO_NEW는 오직 (f)에서만 (inserter가 동시). 그 외는 모두
TOO_OLD. 순서 — (e)가 (f)보다 먼저인 이유는 자기 자신의 delete를 경계가
아닌 정체성으로 결정하기 때문이다. (f)가 (g)보다 먼저인 이유는 in-snapshot
inserter가 “뒤로 봐야 한다”의 더 강한 이유이기 때문이다. (g)를 먼저 테스트하면
한 동시 트랜잭션의 insert-and-delete가 잘못 SNAPSHOT_SATISFIED를 돌려 절대
커밋되지 않은 행을 노출한다.
flowchart TD
E{"DELETED_BY_ME?"}
E -->|"yes"| EV["TOO_OLD_FOR_SNAPSHOT\nDELETED_CURR_TRAN / INVISIBLE"]
E -->|"no"| F{"inserter IN_SNAPSHOT?"}
F -->|"yes"| FV["TOO_NEW_FOR_SNAPSHOT\nINSERTED_DELETED / INVISIBLE"]
F -->|"no"| G{"deleter IN_SNAPSHOT?"}
G -->|"yes"| GV["SNAPSHOT_SATISFIED\nDELETED_OTHER_TRAN / VISIBLE"]
G -->|"no"| HV["TOO_OLD_FOR_SNAPSHOT\nDELETED_COMMITTED[_LOST] / INVISIBLE"]
Figure 6-3 — deleted ladder. deleter가 여전히 동시일 때만 보임.
6.6 TOO_NEW가 버전 체인 추적을 구동하는 방식
섹션 제목: “6.6 TOO_NEW가 버전 체인 추적을 구동하는 방식”mvcc_satisfies_snapshot은 prev_version_lsa를 절대 건드리지 않는다. 그저
TOO_NEW_FOR_SNAPSHOT을 내보내 호출자에게 추적하라고 알린다 (링크는 insert/update
시점에 설정된다, Ch. 3). 스캔/fetch 계층 (heap_file.c)이 판정을 해석한다 —
SNAPSHOT_SATISFIED는 버전 반환. TOO_OLD는 체인 head가 죽었다는 뜻. TOO_NEW는
prev_version_lsa를 dereference하여 이전 헤더를 로드하고 다시 호출. 추적은
종료된다 — 각 링크의 inserter MVCCID는 이전 링크보다 엄격히 오래되었으므로
결국 in-snapshot 테스트가 실패하거나 (case d) null LSA에서 체인이 끝난다.
TOO_NEW를 내놓는 경우는 (c)와 (f)뿐이다.
6.7 insert/delete 비대칭 — worked example
섹션 제목: “6.7 insert/delete 비대칭 — worked example”reader R이 경계 [lowest=100, highest=100)인 스냅샷을 들고 있다 — 트랜잭션
100이 R이 아는 유일한 활성 id이며 100이 무엇이든 하기 직전에 잡힌 것이다.
§6.4–§6.5 ladder에 네 시나리오를 대본다.
| 시나리오 | 헤더 | 헬퍼 결과 | Case | 판정 |
|---|---|---|---|---|
| 커밋된 T50 insert, 삭제 안 됨 | ins=50, DELID 없음 | inserter not in-snapshot (50 < 100) | (d) | SNAPSHOT_SATISFIED |
| 동시 T100 insert, 미커밋 | ins=100, DELID 없음 | inserter in-snapshot (100 >= 100) | (c) | TOO_NEW -> prev 추적 |
| T50 insert, 동시 T100 delete | ins=50, del=100 | inserter not in-snapshot. deleter in-snapshot | (g) | SNAPSHOT_SATISFIED |
| T50 insert, 커밋된 T60 delete | ins=50, del=60 | 둘 다 not in-snapshot | (h) | TOO_OLD |
행 2와 3이 비대칭이다 — too-new insert는 reader를 더 오래된 버전을 찾으러 뒤로 보낸다. too-new delete는 행을 보이게 유지한다 (R 입장에서 미커밋된 delete는 일어나지 않았다). insert MVCCID는 버전의 등장을 게이트하고, delete MVCCID는 사라짐을 게이트한다.
6.8 성능 모니터링 leaf
섹션 제목: “6.8 성능 모니터링 leaf”tracking이 켜져 있으면 모든 leaf는 perfmon_mvcc_snapshot(thread_p, snapshot_type, rec_type, visibility)를 호출한다 — snapshot_type은 항상
PERF_SNAPSHOT_SATISFIES_SNAPSHOT, rec_type이 왜를 분류, visibility가
결과다. 호출은 perfmon_is_perf_tracking_and_active (PERFMON_ACTIVATION_FLAG_MVCC_SNAPSHOT)로 가드되므로 꺼져 있으면 hot path는
공짜다. leaf당 rec_type 버킷은 PERF_SNAPSHOT_RECORD_<suffix>다. Figure 6-2/6-3가
이미 여덟 개 suffix와 visibility 비트를 보여준다 — (a) INSERTED_VACUUMED,
(b) INSERTED_CURR_TRAN, (c) INSERTED_OTHER_TRAN, (d) INSERTED_COMMITED,
(e) DELETED_CURR_TRAN, (f) INSERTED_DELETED, (g) DELETED_OTHER_TRAN, (h)
DELETED_COMMITTED. 두 leaf는 한 단계 더 갈린다 — (d)와 (h)는 각각 _LOST
변종 — INSERTED_COMMITED_LOST (d’)와 DELETED_COMMITTED_LOST (h’)를 가진다.
_LOST 변종이 흥미로운 부분이다. 커밋된 inserter leaf (d)에서 코드는 (d)와
(d’) 사이를 결정하기 전에 한 번 더 묻는다.
// mvcc_satisfies_snapshot -- src/transaction/mvcc.c (case d, perfmon detail) if (rec_header->mvcc_ins_id != MVCCID_ALL_VISIBLE && vacuum_is_mvccid_vacuumed (rec_header->mvcc_ins_id)) { perfmon_mvcc_snapshot (thread_p, PERF_SNAPSHOT_SATISFIES_SNAPSHOT, PERF_SNAPSHOT_RECORD_INSERTED_COMMITED_LOST, PERF_SNAPSHOT_VISIBLE); } else { perfmon_mvcc_snapshot (thread_p, PERF_SNAPSHOT_SATISFIES_SNAPSHOT, PERF_SNAPSHOT_RECORD_INSERTED_COMMITED, PERF_SNAPSHOT_VISIBLE); }vacuum_is_mvccid_vacuumed (vacuum.h에 있음)는 MVCCID가 vacuum의 oldest-visible
워터마크 (Ch. 9)보다 오래되면 true를 반환한다. 즉 vacuum이 이 도장을 떼어낼
자격이 있었지만 버전이 아직 도장을 들고 있다. 따라서 _LOST는 vacuum이 제거
했어야 할 도장을 여전히 입고 있는 버전 수 — vacuum 지연 측정치다.
!= MVCCID_ALL_VISIBLE 가드는 all-visible sentinel을 건너뛴다 (실제 id가 절대로
아니다). delete-side _LOST leaf (h’)는 mvcc_del_id에 대한 대칭 탐색이며,
delete 도장은 절대로 sentinel이 아니므로 가드가 없다.
6.9 가시성 불변식, 정확하게
섹션 제목: “6.9 가시성 불변식, 정확하게”가시성 불변식. 빌드된 (non-dirty) 스냅샷 S와 헤더 H에 대해
mvcc_satisfies_snapshot은 H의 inserter가 S에 보이고 (committed-before, 또는
나, 또는 vacuum-stripped) AND H의 deleter가 아직 보이지 않을 때 (delete 도장
없음, 또는 deleter가 동시) iff SNAPSHOT_SATISFIED를 반환한다. TOO_NEW_FOR_SNAPSHOT은
정확히 inserter가 동시일 때 (delete 상태에 무관). TOO_OLD_FOR_SNAPSHOT은 정확히
inserter가 보이지만 deleter도 visible-or-me일 때. §6.4와 §6.5의 side별 순서
논증이 이를 강제한다. 어떤 재배열이든 phantom row (보이는 too-new 버전)나
사라진 row (보이지 않는 committed-before 버전)을 만든다.
6.10 Cross-check 노트
섹션 제목: “6.10 Cross-check 노트”- case (f)의
!!TODO. 주석은 inserter-in-snapshot 테스트가 deleter 테스트와 독립적으로 도달 가능한지를 의심한다. 흔한 경우 (한 트랜잭션이 insert와 delete를 모두 함)에는 맞지만, 제거가 명백히 안전하지는 않다 — 동시 T_a가 insert한 행이 동시 T_b에 의해 delete된 경우 (a != b, 둘 다 in-snapshot)라면, (f) 없이 (g)로 떨어져SNAPSHOT_SATISFIED를 반환해 insert가 절대 커밋되지 않은 행을 노출한다. 그런 헤더가 발생할 수 있는지는mvcc.c외부의 락 규칙에 달려 있다. 이 분기는 load-bearing으로 다루는 것이 안전하다. - 부수 효과 없음.
mvcc_satisfies_dirty(같은 파일)는snapshot->lowest_active_mvccid/highest_completed_mvccid를 변경한다.snapshot_fnc가 어느 것이 실행될지 선택한다. Ch. 7이 dirty/delete/vacuum 형제를 다룬다.
6.11 챕터 요약 — 핵심 정리
섹션 제목: “6.11 챕터 요약 — 핵심 정리”mvcc_satisfies_snapshot은 순수하고 부수 효과가 없으며,MVCC_IS_HEADER_DELID_VALID의 상위 분기 하나가SNAPSHOT_SATISFIED,TOO_NEW_FOR_SNAPSHOT, 또는TOO_OLD_FOR_SNAPSHOT을 만든다.- not-deleted ladder의 네 정렬된 경우 — insert 도장 없음 (보임), inserted-by-me
(보임), inserter-in-snapshot (
TOO_NEW), 잔여 committed-before (보임). in-snapshot inserter만 뒤로 돌아본다. - deleted ladder의 네 정렬된 경우 — deleted-by-me (
TOO_OLD), inserter-in-snapshot (TOO_NEW), deleter-in-snapshot (보임), 잔여 (TOO_OLD). deleter가 여전히 동시일 때만 보인다. - 비대칭 — too-new insert는 이 버전을 숨긴다 (더 오래된 것을 보라). too-new
delete는 보이게 유지한다.
TOO_NEW는prev_version_lsa를 뒤로 추적.TOO_OLD는 종착. - 모든 leaf는
PERF_SNAPSHOT_*버킷을 보고한다._LOST버킷은vacuum_is_mvccid_vacuumed가 도장이 이미 사라졌어야 한다고 말할 때 발화한다 — vacuum 지연 측정치. insert side에서는MVCCID_ALL_VISIBLE에 대한 가드가 있다. - 가시성 불변식 — 보이는 것은 inserter가 보이고 deleter가 아직 보이지 않을 때.
TOO_NEW는 inserter가 동시.TOO_OLD는 inserter가 보이지만 deleter가 visible-or-me. case 순서 (정체성이 경계보다 먼저)가 이를 강제한다. - deleted 분기의 inserter 테스트에 대한
!!TODO는 미해결 redundancy 질문이지 죽은 코드가 아니다 — 제거하면 두 별개의 동시 트랜잭션에서 insert와 delete가 온 경우 절대 커밋되지 않은 행을 노출할 위험이 있다.
Chapter 7: Delete, Dirty, Vacuum용 형제 술어들
섹션 제목: “Chapter 7: Delete, Dirty, Vacuum용 형제 술어들”mvcc_satisfies_snapshot (Chapter 6)이 읽기에 답한다. 하지만 같은
MVCC_REC_HEADER — Chapter 3의 네 필드짜리 on-record 도장 — 는 같은 바이트로
다른 판정을 필요로 하는 세 호출자에게 심문당한다. delete/update할
writer는 liveness가 필요하다 (block해야 할 진행 중인 deleter가 있는가?).
dirty 읽기 (mvcc_satisfies_dirty)는 진행 중인 writer의 효과를 봐야
하고 어떤 MVCCID를 활성으로 다루었는지 보고해야 한다. vacuum은 죽은
버전이 어떤 실행 중 트랜잭션도 더 이상 필요로 하지 않을 만큼 오래되었는지
알아야 한다.
각 술어는 비교 하나를 다르게 기울인다 — 동결된 스냅샷 멤버십 vs 원시
liveness vs 정적 워터마크. 그 축은 7.7의 대조표가 담는다. 헤더 디코드 관용구
(MVCC_IS_HEADER_DELID_VALID, MVCC_IS_FLAG_SET)는 Chapter 3과 6에서 가정한다.
7.1 두 비교 primitive — ACTIVE vs IN-SNAPSHOT
섹션 제목: “7.1 두 비교 primitive — ACTIVE vs IN-SNAPSHOT”모든 것이 두 헬퍼 매크로 패밀리에 달려 있다. _ACTIVE 패밀리는 live 탐색을,
_IN_SNAPSHOT 패밀리는 동결된 탐색을 감싼다 (DELETER 변종은 mvcc_ins_id
대신 mvcc_del_id를 넘긴다).
// MVCC_IS_REC_{INSERTER,DELETER}_{ACTIVE,IN_SNAPSHOT} -- src/transaction/mvcc.c#define MVCC_IS_REC_INSERTER_ACTIVE(thread_p, rec_header_p) \ (mvcc_is_active_id (thread_p, (rec_header_p)->mvcc_ins_id))#define MVCC_IS_REC_INSERTER_IN_SNAPSHOT(thread_p, rec_header_p, snapshot) \ (mvcc_is_id_in_snapshot (thread_p, (rec_header_p)->mvcc_ins_id, (snapshot)))mvcc_is_id_in_snapshot은 동결 테스트다 — 캡처된 스냅샷 경계와 비트
영역에 비교한다 (Chapter 4). “이 writer가 스냅샷이 잡힌 순간에 활성이었는가?”
mvcc_is_active_id는 live 테스트다 — 캡처된 사본이 아닌 현재 전역
active-set을 참조한다.
// mvcc_is_active_id -- src/transaction/mvcc.c if (MVCC_ID_PRECEDES (mvccid, curr_mvcc_info->recent_snapshot_lowest_active_mvccid)) return false; /* below recent watermark: long dead */ if (logtb_is_current_mvccid (thread_p, mvccid)) return true; /* mine (or my sub-tx) */ return log_Gl.mvcc_table.is_active (mvccid); /* live global probe, not the snapshot copy */불변식 — delete는 liveness를, read는 동결된 스냅샷을 쓴다. reader는 안정된 뷰를 위해 캡처된 스냅샷 (
mvcc_is_id_in_snapshot)을 탐색한다. writer는 live set (mvcc_is_active_id)을 탐색하고 지금 활성인 모든 deleter에 block한다 — 그래야 업데이트를 잃지 않는다. 둘을 바꾸면 lost update 가 생기거나, 읽기가 post-snapshot 커밋에 대해 deadlock한다.
워터마크 매크로 MVCC_IS_REC_{INSERTED,DELETED}_SINCE_MVCCID (vacuum 전용)는
순수 산술이다 — INSERTED_SINCE는 !MVCC_ID_PRECEDES(ins_id, mvcc_id), 즉
ins_id >= mvcc_id이며 테이블 탐색 없음.
7.2 결과 struct/enum 정리
섹션 제목: “7.2 결과 struct/enum 정리”네 술어는 세 개의 enum을 반환한다. mvcc_satisfies_snapshot_result (Chapter 6
에서)는 ..._dirty와 ..._is_not_deleted_for_snapshot이 더 좁은 영역으로
재사용한다.
mvcc_satisfies_snapshot_result (..._dirty,
..._is_not_deleted_for_snapshot 반환):
| Field | Role | Why it exists |
|---|---|---|
TOO_OLD_FOR_SNAPSHOT | 안 보임. 내가 삭제했거나 커밋된 tx가 삭제 | ”영원히 죽음”과 “아직 태어나지 않음”을 구분해 chain walker가 멈추게 |
SNAPSHOT_SATISFIED | 보이고 유효 | 유일한 “yes” 답 |
TOO_NEW_FOR_SNAPSHOT | 안 보임. inserter가 여전히 활성 — prev_version_lsa 추적 | mvcc_satisfies_snapshot만 반환 (7.4, 7.5는 절대 반환 안 함) |
mvcc_satisfies_delete_result (..._delete 반환):
| Field | Role | Why it exists |
|---|---|---|
DELETE_RECORD_INSERT_IN_PROGRESS | 다른 활성 tx가 insert | 아직 committed-visible 아님. 건드리지 말 것 |
DELETE_RECORD_CAN_DELETE | 보이고 유효 — 진행 | all-visible, by-me, 또는 inserter-committed |
DELETE_RECORD_DELETED | 커밋된 tx가 삭제 | 대상 사라짐. 호출자는 serialization/not-found 발생 |
DELETE_RECORD_DELETE_IN_PROGRESS | 다른 활성 tx가 삭제 | 호출자는 deleter에 대기해야 함 (lock manager 핸드오프, cubrid-lock-manager-detail 참고) 후 재시도 |
DELETE_RECORD_SELF_DELETED | 이 tx가 삭제 | idempotent. 제거됨으로 취급 |
mvcc_satisfies_vacuum_result (..._vacuum 반환):
| Field | Role | Why it exists |
|---|---|---|
VACUUM_RECORD_REMOVE | 레코드 전체를 물리적으로 제거 | deleter가 oldest reader 이전에 커밋 — 다시는 보지 못함 |
VACUUM_RECORD_DELETE_INSID_PREV_VER | 레코드 유지. insert MVCCID와 prev_version_lsa 떼어내기 | all-visible-and-live. 도장과 체인은 죽은 무게. 행은 여전히 필요 |
VACUUM_RECORD_CANNOT_VACUUM | 그대로 두기 | 이미 vacuum 됐거나, 최근 insert/delete — 실행 중 tx가 필요할 수 있음 |
7.3 mvcc_satisfies_delete — 다섯 상태 liveness 판정
섹션 제목: “7.3 mvcc_satisfies_delete — 다섯 상태 liveness 판정”DELETE나 UPDATE가 자신이 변경하려는 힙 행에 대해 실행하는 술어다. snapshot
인자를 받지 않는다 — liveness는 항상 “지금”이다. 상위 분기는
MVCC_IS_HEADER_DELID_VALID다 — 이 행이 delete 도장을 들고 있는가?
not-yet-deleted 분기 (!MVCC_IS_HEADER_DELID_VALID):
// mvcc_satisfies_delete -- src/transaction/mvcc.c if (!MVCC_IS_HEADER_DELID_VALID (rec_header)) { if (!MVCC_IS_FLAG_SET (rec_header, OR_MVCC_FLAG_VALID_INSID)) return DELETE_RECORD_CAN_DELETE; /* no insid stamp: all-visible */ if (MVCC_IS_REC_INSERTED_BY_ME (thread_p, rec_header)) return DELETE_RECORD_CAN_DELETE; /* only I can see it; safe to drop */ else if (MVCC_IS_REC_INSERTER_ACTIVE (thread_p, rec_header)) return DELETE_RECORD_INSERT_IN_PROGRESS; /* another tx is still inserting */ else /* inserter committed; ... perfmon ... */ return DELETE_RECORD_CAN_DELETE; }네 개의 종착 하위 분기 — VALID_INSID 없음 (insid가 vacuum됨, Chapter 9),
inserted-by-me, inserter-committed는 모두 CAN_DELETE를 낳는다. ACTIVE inserter만
INSERT_IN_PROGRESS를 낳는다. else perfmon 블록은 통계용으로 “committed”와
“committed-then-insid-vacuumed”만 나누고 결과는 그대로다.
already-deleted 분기 (else) — 대칭적 3방향 분기, 같은 live 탐색:
// mvcc_satisfies_delete -- src/transaction/mvcc.c (else arm) else if (MVCC_IS_REC_DELETED_BY_ME (thread_p, rec_header)) return DELETE_RECORD_SELF_DELETED; else if (MVCC_IS_REC_DELETER_ACTIVE (thread_p, rec_header)) return DELETE_RECORD_DELETE_IN_PROGRESS; /* must WAIT on that deleter */ else /* ... perfmon ... */ return DELETE_RECORD_DELETED; /* deleter committed: target gone */세 개의 종착 하위 분기 — deleted-by-me → SELF_DELETED. deleter-committed →
DELETED. deleter가 여전히 ACTIVE → DELETE_IN_PROGRESS — live 탐색이
필요한 경우다. 호출자는 진행 중인 deleter 뒤에서 block하고 다시 읽는다 (동결된
스냅샷은 post-snapshot deleter를 놓쳐 update를 잃을 수 있다). Figure 7-1이
양쪽 DELID_VALID arm을 다섯 판정에 매핑한다.
flowchart TD A["DELID valid?"] -->|no| B["ins 상태?"] A -->|yes| F["del 상태?"] B -->|else| C1["CAN_DELETE"] B -->|ins ACTIVE| G1["INSERT_IN_PROGRESS"] F -->|mine| H1["SELF_DELETED"] F -->|del ACTIVE| H2["DELETE_IN_PROGRESS"] F -->|committed| H3["DELETED"]
7.4 mvcc_satisfies_dirty — 부수 효과가 있는 술어
섹션 제목: “7.4 mvcc_satisfies_dirty — 부수 효과가 있는 술어”mvcc_satisfies_dirty는 read-uncommitted 가시성을 답한다 — dirty read는 커밋된
것과 진행 중인 것 모두의 효과를 본다. 함수 헤더는 그것이 부수 효과를 가지며,
snapshot->lowest_active_mvccid와 snapshot->highest_completed_mvccid를
변경한다고 경고한다 — 그리고 snapshot 인자는 절대로 트랜잭션 스냅샷이
아니다. 여기서 snapshot은 두 스칼라가 출력 채널인 스크래치 구조체다.
술어는 둘 다 미리 0으로 만들고, 7.3과 같은 DELID_VALID 분기를 live ACTIVE
탐색과 함께 따라가며, 상호 배타적인 두 ACTIVE arm 중 정확히 하나에서
스칼라를 찍는다.
// mvcc_satisfies_dirty -- src/transaction/mvcc.c snapshot->lowest_active_mvccid = MVCCID_NULL; /* both scalars cleared up front */ snapshot->highest_completed_mvccid = MVCCID_NULL; // ... not-deleted arm: only the ACTIVE-inserter branch writes a scalar ... else if (MVCC_IS_REC_INSERTER_ACTIVE (thread_p, rec_header)) snapshot->lowest_active_mvccid = MVCC_GET_INSID (rec_header); /* side effect, then SATISFIED */ // ... already-deleted arm: only the ACTIVE-deleter branch writes a scalar ... else if (MVCC_IS_REC_DELETER_ACTIVE (thread_p, rec_header)) snapshot->highest_completed_mvccid = rec_header->mvcc_del_id; /* side effect, then SATISFIED */분기 모양은 7.3과 같다. not-deleted arm, 네 개의 하위 분기 모두
SNAPSHOT_SATISFIED — VALID_INSID 없음 / inserted-by-me / inserter-committed는
아무것도 안 쓴다. inserter ACTIVE만 lowest_active_mvccid를 찍는다.
already-deleted arm, 세 개의 하위 분기 — deleted-by-me와 deleter-committed
→ TOO_OLD_FOR_SNAPSHOT. deleter ACTIVE만 → SNAPSHOT_SATISFIED,
highest_completed_mvccid를 찍음. dirty는 절대 TOO_NEW를 반환하지 않는다 —
활성 inserter를 보이는 것으로 받아들인다.
불변식 — dirty의 두 스칼라는 트랜잭션 뷰가 아니라 출력이다. 실제 스냅샷에서 (Chapter 5–6) 이 필드들은 active-set의 경계를 짓는 캡처된 입력이지만, 여기서는 일회용 구조체에 대한 출력이며, 호출당 최대 하나만 non-NULL이다. live 스냅샷을 넘기면 그 경계가 손상된다 — 그래서 헤더 노트가 있다.
7.5 mvcc_is_not_deleted_for_snapshot — 값싼 여전히-삭제 가능한지 체크
섹션 제목: “7.5 mvcc_is_not_deleted_for_snapshot — 값싼 여전히-삭제 가능한지 체크”가장 가벼운 술어 — “이 행이 내 스냅샷 뷰에서 삭제되지 않은 상태인가?” — 호출자가 다른 이유로 그 행이 보이는 것을 이미 알 때 쓴다. delete와 dirty와는 달리, 동결된 IN-SNAPSHOT 의미론을 쓴다.
// mvcc_is_not_deleted_for_snapshot -- src/transaction/mvcc.c if (!MVCC_IS_HEADER_DELID_VALID (rec_header)) return SNAPSHOT_SATISFIED; /* never deleted: trivially "not deleted" */ else if (MVCC_IS_REC_DELETED_BY_ME (thread_p, rec_header)) return TOO_OLD_FOR_SNAPSHOT; /* I deleted it: gone for me */ else if (MVCC_IS_REC_DELETER_IN_SNAPSHOT (thread_p, rec_header, snapshot)) /* frozen */ return SNAPSHOT_SATISFIED; /* deleter active/after-snapshot: still here */ else return TOO_OLD_FOR_SNAPSHOT; /* deleter committed before snapshot: gone */네 개의 종착 분기 — not-deleted short-circuit과 3방향 deleted arm — insert
쪽 로직 없음 (호출자의 일), TOO_NEW 없음. deleter 테스트는
MVCC_IS_REC_DELETER_IN_SNAPSHOT (동결)이지 delete의 ACTIVE 탐색이 아니다.
이것이 read 스타일 판정이기 때문이다. in-snapshot deleter (활성, 또는 스냅샷
이후에 커밋)는 아직 보이지 않으므로 행은 “삭제되지 않음”으로 카운트된다.
7.6 mvcc_satisfies_vacuum — 3방향 워터마크 판정
섹션 제목: “7.6 mvcc_satisfies_vacuum — 3방향 워터마크 판정”Vacuum은 oldest_mvccid만 받는다 — Chapter 9의 oldest-active 워터마크다. 그
아래로는 어떤 실행 중 트랜잭션도 버전을 필요로 할 수 없으므로 결정은 순수
산술이다. 바깥 분기는 “삭제되지 않음, 또는 통째로 제거하기에 너무 최근 에
삭제됨 (del_id >= oldest)“이다.
// mvcc_satisfies_vacuum -- src/transaction/mvcc.c if (!MVCC_IS_HEADER_DELID_VALID (rec_header) || MVCC_IS_REC_DELETED_SINCE_MVCCID (rec_header, oldest_mvccid)) { if (!MVCC_IS_HEADER_INSID_NOT_ALL_VISIBLE (rec_header) || MVCC_IS_REC_INSERTED_SINCE_MVCCID (rec_header, oldest_mvccid)) return VACUUM_RECORD_CANNOT_VACUUM; /* insid gone OR inserted too recently; ...perfmon... */ else return VACUUM_RECORD_DELETE_INSID_PREV_VER; /* inserter committed before oldest: insid dead weight */ } else return VACUUM_RECORD_REMOVE; /* deleter committed before oldest: nobody sees it */세 가지 종착 결과, 모든 분기 정리 — (1) REMOVE (바깥 else) —
del_id < oldest_mvccid. (2) CANNOT_VACUUM (첫 arm) — insid가 사라졌거나
(!INSID_NOT_ALL_VISIBLE) inserted-since-oldest (ins_id >= oldest_mvccid).
(3) DELETE_INSID_PREV_VER (안쪽 else) — inserter가 oldest_mvccid
이전에 커밋했고 insid가 여전히 있어서, insert 도장과 prev_version_lsa가 죽은
메타데이터다.
불변식 — vacuum은 단일 전역 워터마크 너머만 본다. 모든 비교는
header_idvsoldest_mvccid다 — 스냅샷도, live 탐색도 없다. 워터마크는 단조 비-감소 (Chapter 9)이므로,REMOVE판정이 나중에 필요해질 수는 없다.oldest_mvccid가 너무 높게 설정되면 여전히 보이는 버전을 vacuum할 수 있다 — 그래서 Chapter 9는 보수적으로 계산한다.
7.7 네 술어, 하나의 헤더 — 대조표
섹션 제목: “7.7 네 술어, 하나의 헤더 — 대조표”| 술어 | 추가 입력 | 비교 primitive | 진행 중 writer 보는가? | 결과 영역 | 부수 효과 |
|---|---|---|---|---|---|
mvcc_satisfies_snapshot (Ch.6) | 트랜잭션 스냅샷 | 동결 IN-SNAPSHOT | 아니오 (active inserter → TOO_NEW) | 3-state snapshot | 없음 |
mvcc_is_not_deleted_for_snapshot | 스냅샷 | 동결 IN-SNAPSHOT (deleter만) | 아니오 | 3중 2 snapshot (no TOO_NEW) | 없음 |
mvcc_satisfies_dirty | 스크래치 스냅샷 구조체 | live ACTIVE | 예 (active writer → SNAPSHOT_SATISFIED) | 3중 2 snapshot (no TOO_NEW) | lowest_active/highest_completed 씀 |
mvcc_satisfies_delete | 없음 | live ACTIVE | 예, 대기 신호로 | 5-state delete | 없음 |
mvcc_satisfies_vacuum | oldest_mvccid 워터마크 | 워터마크 SINCE | 아니오 — 완전히 과거인 버전만 작용 | 3-state vacuum | 없음 |
중간 열만이 이들을 구분한다. DELID_VALID 분기, inserted-by-me short-circuit,
perfmon 부기는 공통이다.
7.8 챕터 요약 — 핵심 정리
섹션 제목: “7.8 챕터 요약 — 핵심 정리”- 네 술어의 차이는 거의 전적으로 하나의 비교 primitive다 —
mvcc_is_id_in_snapshot(동결, reads),mvcc_is_active_id(live, delete와 dirty), 또는oldest_mvccid에 대한MVCC_ID_PRECEDES(워터마크, vacuum).mvcc_satisfies_snapshot만이TOO_NEW를 반환한다. mvcc_satisfies_delete는 live ACTIVE 탐색을 써서 writer가 진행 중인 deleter 뒤에서 block한다 (DELETE_IN_PROGRESS) — 그래서 lost update를 피한다. 다섯 상태 — 4-분기 not-deleted arm (CAN_DELETE만 빼고 모두)과 3-분기 already-deleted arm (SELF_DELETED/DELETE_IN_PROGRESS/DELETED).mvcc_satisfies_dirty는 유일한 부수 효과 술어이며 역시 live다 —SNAPSHOT_SATISFIED일 때 active inserter를lowest_active_mvccid에 또는 active deleter를highest_completed_mvccid에 (절대 둘 다는 아님) 찍는다 — 스크래치 구조체에 대한 출력이지 절대로 실제 스냅샷이 아니다.mvcc_is_not_deleted_for_snapshot은 값싼 delete 전용 동결 체크 —MVCC_IS_REC_DELETER_IN_SNAPSHOT을 통한 네 분기 (not-deleted short-circuit과 3방향 deleted arm), insert 로직 없음,TOO_NEW없음.mvcc_satisfies_vacuum은 순수 워터마크 산술이다 — deleter가oldest_mvccid이전에 커밋했으면REMOVE, inserter가 그랬으면DELETE_INSID_PREV_VER, 그 외에는CANNOT_VACUUM. 워터마크가 보수적으로 낮기 때문에만 안전하다.
Chapter 8: 커밋과 History Ring 전진
섹션 제목: “Chapter 8: 커밋과 History Ring 전진”쓰기 트랜잭션이 끝나면 세 가지가 일어나야 한다. 그것도 크래시에서 살아남고
lock-free 스냅샷 reader에게도 정확한 순서로 — 트랜잭션의 MVCCID가 전역
active-set에서 비활성으로 표시되고, 새 active-set이 history ring에
publish되어 동시 build_mvcc_info 호출자가 그것을 보게 되며, VACUUM이 신뢰
하는 전역 oldest-active 워터마크가 전진된다 — 다만 아직 미커밋 트랜잭션이
복구를 위해 필요로 할 수 있는 데이터를 VACUUM이 지우지 못할 만큼만. 이 챕터는
그 경로를 끝에서 끝까지 추적한다.
이 구조체들의 read 쪽 (비트 영역 탐색, 캐시된 스칼라, read 시점의 version
recheck)은 상위 동반 문서 cubrid-mvcc.md와 Chapters 4–5에 있다. 여기서는
write 쪽 — 퇴역, publish, 워터마크 유지만 다룬다.
8.1 등장하는 세 구조체
섹션 제목: “8.1 등장하는 세 구조체”커밋은 세 핵심 struct 모두를 건드린다 (mvcc_active_tran의 모든 필드 역할은
Ch. 1에 있다. 커밋과 관련된 유지 필드만 여기 반복한다).
mvcctable — 프로세스 전역 코디네이터 (인스턴스 하나, log_Gl.mvcc_table).
| Field | Role | Why it exists |
|---|---|---|
m_transaction_lowest_visible_mvccids | tran-index당 atomic<MVCCID>. 이 tran이 visible로 유지해야 할 가장 오래된 MVCCID | VACUUM의 floor. 커밋은 committer의 슬롯을 clamp |
m_transaction_lowest_visible_mvccids_size | 그 배열 길이 | realloc 가드 |
m_current_status_lowest_active_mvccid | atomic 전역 oldest-active 워터마크 | advance_oldest_active가 CAS-bump. VACUUM이 read |
m_current_trans_status | mutex 하에서 변경되는 살아있는 mvcc_trans_status | lock-free로는 절대 read 안 함 |
m_trans_status_history_position | 가장 최근 publish된 status의 atomic ring index | 단일 가시성 store |
m_trans_status_history | HISTORY_MAX_SIZE (2048) status 슬롯의 ring | lock-free reader가 최근 스냅샷을 가져감 |
m_active_trans_mutex | status 변경 직렬화 | 한 번에 한 completer |
m_new_mvccid_lock, m_oldest_visible, m_ov_lock_count | MVCCID 발급 + oldest-visible 워터마크 | 커밋 경로 밖. Ch. 3과 Ch. 9 |
mvcc_trans_status — 하나의 전역 “스냅샷 세대”.
| Field | Role | Why it exists |
|---|---|---|
m_active_mvccs | mvcc_active_tran 페이로드 | active-set 데이터 |
m_last_completed_mvccid | 이 status에 퇴역한 마지막 MVCCID | highest_completed 힌트 |
m_event_type | COMMIT, ROLLBACK, 또는 SUBTRAN | 세대의 사후 |
m_version | 세대마다 증가하는 atomic<version_type> | reader recheck 토큰 |
mvcc_active_tran — 커밋 시 유지 측의 모든 필드.
| Field | Role at commit | Why it exists |
|---|---|---|
m_bit_area | 500 uint64_t 워드. 비트 set = MVCCID 커밋됨 | O(1) 최근 MVCCID 상태 |
m_bit_area_start_mvccid | 비트 0에 매핑된 MVCCID | 윈도 베이스. ltrim_area가 전진 |
m_bit_area_length | 윈도 길이 (비트 단위) | set_bitarea_mvccid가 키우고 trim이 줄임 |
m_long_tran_mvccids | 윈도보다 오래된 활성 MVCCID의 정렬 배열 | 윈도가 잔여를 지나 미끄러질 수 있게 |
m_long_tran_mvccids_length | long-tran 항목 수 | 배열 범위. compute_lowest_active_mvccid 구동 |
m_initialized | 생명주기 플래그. copy_to와 reset_start_mvccid가 assert | init/finalize idempotency 가드. use-before-init 차단 |
flowchart LR CUR["m_current_trans_status\n(live, mutex 하)"] -->|copy_to THREAD_SAFE| RING["m_trans_status_history[2048]"] POS["m_trans_status_history_position"] -->|newest| RING CUR -->|set_inactive_mvccid| AT["m_active_mvccs\nm_bit_area / m_long_tran_mvccids"] CUR -.->|compute_lowest + CAS| LOW["m_current_status_lowest_active_mvccid"]
Figure 8-1 — live status가 publish ring (copy_to), active-set
(set_inactive_mvccid), 워터마크 (CAS)를 채운다.
8.2 logtb_complete_mvcc — 호출자와 read-only 빠른 경로
섹션 제목: “8.2 logtb_complete_mvcc — 호출자와 read-only 빠른 경로”logtb_complete_mvcc (log_tran_table.c)는 모든 커밋과 롤백에서 실행되며,
트랜잭션이 퇴역할 MVCCID를 가지고 있는지부터 먼저 결정한다.
// logtb_complete_mvcc -- src/transaction/log_tran_table.cmvccid = curr_mvcc_info->id;tran_index = LOG_FIND_THREAD_TRAN_INDEX (thread_p);
if (MVCCID_IS_VALID (mvccid)) { mvcc_table->complete_mvcc (tran_index, mvccid, committed); /* <- write tran: full path */ }else { if (committed && logtb_tran_update_all_global_unique_stats (thread_p) != NO_ERROR) { assert (false); } /* read-only tran never allocated an MVCCID; just drop its visibility floor */ log_Gl.mvcc_table.reset_transaction_lowest_active (tran_index); /* <- stores MVCCID_NULL */ }curr_mvcc_info->recent_snapshot_lowest_active_mvccid = MVCCID_NULL;// ... condensed: reset count-optim state, curr_mvcc_info->reset (), perf ...read-only tran (MVCCID 없음)은 어떤 active-set에도 없다 — complete_mvcc를
건너뛰고, reset_transaction_lowest_active가 슬롯에 MVCCID_NULL을 저장해
자신의 floor를 푼다. mutex도 없고 ring 전진도 없다. 아래의 모든 것은 write
경로다.
8.3 complete_mvcc 끝에서 끝까지
섹션 제목: “8.3 complete_mvcc 끝에서 끝까지”본문은 명시적인 ulock.unlock ()까지 m_active_trans_mutex 아래에서 실행된다.
분기 단위 walkthrough.
m_active_trans_mutex잠금.next_trans_status_start(§8.4) — 다음 ring 슬롯 예약, 버전 증가, 슬롯 무효화.if (committed)→logtb_tran_update_all_global_unique_stats. 실패 시assert (false). else (롤백) 건너뜀.set_inactive_mvccid(mvccid)(§8.7), 그다음m_last_completed_mvccid = mvccid와m_event_type = COMMIT/ROLLBACK설정.next_tran_status_finish(§8.5) — active-set 복사, position publish.- clamp 분기 —
if (committed)floor를mvccid까지 위로 clamp (슬롯이MVCCID_NULL이거나 선행할 때만). else floor를MVCCID_NULL로 설정. - 잠금 해제.
- 잠금 해제 후 전진 —
if전역== mvccid또는mvccid가bit_area_start에 선행하면 →compute_lowest_active_mvccid호출, 그 다음if버전 불변이면 →advance_oldest_active. else stale 결과 건너뜀. else 전체 건너뜀.
단계 6 (clamp 분기)는 committer 자신의 슬롯
m_transaction_lowest_visible_mvccids[tran_index]를 조정한다.
// mvcctable::complete_mvcc -- src/transaction/mvcc_table.cppif (committed) { /* be sure that transaction modifications can't be vacuumed up to LOG_COMMIT. ... * It will be set to NULL after LOG_COMMIT */ MVCCID tran_lowest_active = oldest_active_get (m_transaction_lowest_visible_mvccids[tran_index], ...); if (tran_lowest_active == MVCCID_NULL || MVCC_ID_PRECEDES (tran_lowest_active, mvccid)) { oldest_active_set (..., mvccid, ...); /* <- clamp UP to mvccid, never down */ } }else { oldest_active_set (..., MVCCID_NULL, ...); /* <- rollback releases the floor immediately */ }불변식 — VACUUM은 커밋 중인 트랜잭션의
LOG_COMMIT이전을 통과해서는 안 된다. 커밋은 슬롯을mvccid까지 올린다 (단,MVCCID_NULL이거나 엄격히 더 오래된 경우만).LOG_COMMIT이 영구 저장될 때까지 VACUUM을mvccid이하로 묶는다.LOG_COMMIT후에만MVCCID_NULL로 reset된다. 강제 수단: clamp 조건이 여기서 floor를 절대 낮추지 않는다. 위반 시: VACUUM이 이 tran의 변경을 지우고,LOG_COMMIT이전 크래시는 그것을 복구 불가능하게 만든다. 롤백은 이 위험이 없으므로 floor를 즉시 떨어뜨린다.
8.4 next_trans_status_start — 예약과 무효화
섹션 제목: “8.4 next_trans_status_start — 예약과 무효화”// mvcctable::next_trans_status_start -- src/transaction/mvcc_table.cppnext_index = (m_trans_status_history_position.load () + 1) & HISTORY_INDEX_MASK; /* ring +1 */next_version = ++m_current_trans_status.m_version; /* bump GLOBAL version */mvcc_trans_status &next_trans_status = m_trans_status_history[next_index];next_trans_status.m_version.store (next_version); /* poison the target slot */return next_trans_status;mutex 하의 세 효과 — ring index가 HISTORY_MAX_SIZE (2048, 2의 거듭제곱
이므로 & HISTORY_INDEX_MASK가 wrap)로 전진. 현재 status의 버전 증가
(reader recheck 토큰). 대상 슬롯의 버전이 페이로드가 존재하기 전에
next_version으로 stamp.
불변식 — 절반 쓰여진 슬롯은 감지 가능하게 stale하며, publish는 마지막 store다. 슬롯의
m_version은 비트 영역이 채워지기 전에 stamp된다.next_tran_status_finish(§8.5)가 페이로드를 채우고 그 다음에야m_trans_status_history_position을 store한다 — 단일 publish다. 버전은 평이한 단조 증가unsigned int(version_type)이며 세대당 +1이다. reader의 recheck는 정확한 값 비교다 (next_status.m_version.load () == next_version), seqlock parity가 아니다. 강제 수단: stamp-first + publish-last 문장 순서와 reader recheck (Ch. 4/5). 위반 시: reader가 전진된 버전을 부분 비트 위에서 매칭해 찢어진 active-set을 받을 수 있다.
8.5 next_tran_status_finish — publish는 마지막
섹션 제목: “8.5 next_tran_status_finish — publish는 마지막”// mvcctable::next_tran_status_finish -- src/transaction/mvcc_table.cppm_current_trans_status.m_active_mvccs.copy_to (next_trans_status.m_active_mvccs, mvcc_active_tran::copy_safety::THREAD_SAFE); /* deep-copy the active set */next_trans_status.m_last_completed_mvccid = m_current_trans_status.m_last_completed_mvccid;next_trans_status.m_event_type = m_current_trans_status.m_event_type;m_trans_status_history_position.store (next_index); /* <- THE publish */copy_safety::THREAD_SAFE가 copy_to로 하여금 source와 destination에 대해
check_valid를 실행하게 한다. 페이로드가 이미 버전 stamp된 슬롯을 채운다.
마지막의 m_trans_status_history_position.store가 publish다 (§8.4 불변식
참조). 그것이 자리 잡기 전까지 reader는 이전 슬롯을 최신으로 본다.
8.6 advance_oldest_active — 잠금 해제 후 CAS 루프
섹션 제목: “8.6 advance_oldest_active — 잠금 해제 후 CAS 루프”잠금 해제 후 complete_mvcc는 퇴역한 mvccid가 병목이었을 때만 전역 워터마크를
다시 계산한다.
// mvcctable::complete_mvcc (post-unlock) -- src/transaction/mvcc_table.cppMVCCID global_lowest_active = m_current_status_lowest_active_mvccid;if (global_lowest_active == mvccid || MVCC_ID_PRECEDES (mvccid, next_status.m_active_mvccs.get_bit_area_start_mvccid ())) { MVCCID new_lowest_active = next_status.m_active_mvccs.compute_lowest_active_mvccid (); if (next_status.m_version.load () == next_version) /* <- recheck: result still ours? */ { advance_oldest_active (new_lowest_active); } }두 가지 트리거 조건. 다음의 경우에만 다시 계산한다 — (a) mvccid가 전역
워터마크와 같음 — 가장 오래된 것이었음 — 또는 (b) mvccid가 슬롯의
bit_area_start_mvccid에 선행 — long tran이 끝났고 floor가 long-tran 배열에
있음. 그 외에는 워터마크가 영향받지 않으므로 작업을 건너뛴다. 버전 recheck.
compute_lowest_active_mvccid는 next_status를 lock-free로 읽는다. 다른
completer가 슬롯을 재사용했다면 버전이 다를 것이고 값은 폐기된다.
// mvcctable::advance_oldest_active -- src/transaction/mvcc_table.cppdo { crt_oldest_active = m_current_status_lowest_active_mvccid.load (); if (crt_oldest_active >= next_oldest_active) { return; } /* <- monotonic guard */ }while (!m_current_status_lowest_active_mvccid.compare_exchange_strong (crt_oldest_active, next_oldest_active));불변식 — 전역 워터마크는 단조 비-감소다.
advance_oldest_active는 절대 올리기만 한다. 강제 수단:crt_oldest_active >= next_oldest_active조기 return이 CAS 루프 안에 있고, 재시도마다 다시 평가된다. 위반 시: VACUUM이 값이 떨어지는 것을 보고 활성 reader가 필요로 하는 데이터를 회수한다. CAS는 경쟁하는 completer를 처리한다 — 더 높은 값이 이기고, 패자는 다시 읽고 빠져나간다.
8.7 set_inactive_mvccid — 퇴역 라우팅
섹션 제목: “8.7 set_inactive_mvccid — 퇴역 라우팅”mutex 안에서 set_inactive_mvccid가 퇴역하는 MVCCID를 라우팅한다.
// mvcc_active_tran::set_inactive_mvccid -- src/transaction/mvcc_active_tran.cppif (MVCC_ID_PRECEDES (mvccid, m_bit_area_start_mvccid)) { remove_long_transaction (mvccid); /* <- slid out of the window: it's a long tran */ }else { set_bitarea_mvccid (mvccid); /* <- still in the window: set its committed bit */ }윈도 베이스보다 오래된 MVCCID는 long-tran 배열에 있다. 그 외에는 비트 영역에
산다 (2-tier 설계, Ch. 4). 비트 영역 분기 (set_bitarea_mvccid)는 그 다음
세 유지 트리거를 실행한다 (§8.9에서 추적).
8.8 remove_long_transaction과 add_long_transaction
섹션 제목: “8.8 remove_long_transaction과 add_long_transaction”// mvcc_active_tran::remove_long_transaction -- src/transaction/mvcc_active_tran.cppassert (m_long_tran_mvccids_length > 0);for (i = 0; i < m_long_tran_mvccids_length - 1; i++) { if (m_long_tran_mvccids[i] == mvccid) { size_t memsize = (m_long_tran_mvccids_length - i - 1) * sizeof (MVCCID); std::memmove (&m_long_tran_mvccids[i], &m_long_tran_mvccids[i + 1], memsize); /* close the gap */ break; } }assert ((i < m_long_tran_mvccids_length - 1) || m_long_tran_mvccids[i] == mvccid);--m_long_tran_mvccids_length;선형 스캔. 매치 시 memmove로 gap을 닫고 (배열을 dense하고 정렬된 채 유지),
길이를 감소시킨다. 루프는 length - 1에서 멈춘다 — 마지막 원소 대상은
본문에서 매치되지 않지만, 후행 assert가 그것이 꼬리였음을 확인하고 무조건적인
--m_long_tran_mvccids_length가 그것을 떨어뜨린다.
add_long_transaction은 역. 마이그레이션 중에만 쓰인다.
// mvcc_active_tran::add_long_transaction -- src/transaction/mvcc_active_tran.cppassert (m_long_tran_mvccids_length < long_tran_max_size ());assert (m_long_tran_mvccids_length == 0 || m_long_tran_mvccids[m_long_tran_mvccids_length - 1] < mvccid);m_long_tran_mvccids[m_long_tran_mvccids_length++] = mvccid; /* append; caller guarantees ascending */불변식 — long-tran 배열은 오름차순이고 경계가 있다. 강제 수단:
add_long_transaction의 두 assert (각 append가 이전 꼬리를 초과. 길이가long_tran_max_size미만). 마이그레이션 source는 비트 영역을 low-to-high로 순회하므로 append는 오름차순이다. 위반 시:compute_lowest_active_mvccid(Ch. 6/9)가m_long_tran_mvccids[0]을 최솟값으로 반환한다 — 정렬되지 않은 배열은 잘못된 워터마크를 낳는다.
8.9 set_bitarea_mvccid — 비트 설정과 유지 트리거
섹션 제목: “8.9 set_bitarea_mvccid — 비트 설정과 유지 트리거”// mvcc_active_tran::set_bitarea_mvccid -- src/transaction/mvcc_active_tran.cppconst size_t CLEANUP_THRESHOLD = UNIT_BIT_COUNT; /* 64 bits */const size_t LONG_TRAN_THRESHOLD = BITAREA_MAX_BITS - long_tran_max_size ();
size_t position = get_bit_offset (mvccid);if (position >= BITAREA_MAX_BITS) { cleanup_migrate_to_long_transations (); /* <- window full: force migration to make room */ position = get_bit_offset (mvccid); /* recompute: start_mvccid moved */ }assert (position < BITAREA_MAX_BITS);if (position >= m_bit_area_length) { m_bit_area_length = position + 1; /* extend; new bits already zero (ALL_ACTIVE) */ }*get_unit_of (position) |= get_mask_of (position); /* set the committed bit */check_valid ();
if (m_bit_area_length > CLEANUP_THRESHOLD) { /* trim all-committed prefix units */ for (first_not_all_committed = 0; first_not_all_committed < get_area_size (); first_not_all_committed++) if (m_bit_area[first_not_all_committed] != ALL_COMMITTED) break; ltrim_area (first_not_all_committed); check_valid (); }if (m_bit_area_length > LONG_TRAN_THRESHOLD) { cleanup_migrate_to_long_transations (); }오버플로 분기. offset이 BITAREA_MAX_BITS (500 × 64 = 32000 bits)에
도달하면, 윈도가 담을 수 없다 — cleanup_migrate_to_long_transations가 윈도를
앞으로 미끄러뜨리고, offset을 새 베이스에 대해 다시 계산하고, assert가
들어맞음을 보장한다. 확장 분기. 현재 길이를 지나는 비트는 단순히
m_bit_area_length를 올린다 — 저장 공간은 이미 0으로 채워져 있으므로
(ALL_ACTIVE) 클리어가 필요 없다. Cleanup threshold. 길이가 한 단위 (64)를
넘으면, 코드가 ALL_COMMITTED가 아닌 첫 단위를 찾고 그 앞 모든 것을
ltrim_area한다 — 값싼 일반 압축. Long-tran threshold. 길이가 여전히
LONG_TRAN_THRESHOLD (비트 영역 최대 - long-tran 용량)를 넘으면, 남은 잔여가
일괄 마이그레이션되어 가능한 모든 활성 tran에 대한 long-tran 슬롯이 정확히
충분히 남는다.
8.10 ltrim_area — 윈도 베이스 미끄러뜨리기
섹션 제목: “8.10 ltrim_area — 윈도 베이스 미끄러뜨리기”// mvcc_active_tran::ltrim_area -- src/transaction/mvcc_active_tran.cppif (trim_size == 0) { return; }size_t new_memsize = (get_area_size () - trim_size) * sizeof (unit_type);if (new_memsize > 0) { std::memmove (m_bit_area, &m_bit_area[trim_size], new_memsize); } /* shift survivors down */size_t trimmed_bits = units_to_bits (trim_size);m_bit_area_length -= trimmed_bits;m_bit_area_start_mvccid += trimmed_bits; /* base advances */std::memset (&m_bit_area[get_area_size ()], ALL_ACTIVE, trim_size * sizeof (unit_type)); /* re-zero tail */ltrim_area는 앞에서 trim_size 단위를 제거한다 — 생존자가 memmove로
아래로 이동, 길이는 trimmed bit 수만큼 줄어든다. m_bit_area_start_mvccid는
같은 양만큼 전진하여 MVCCID↔offset 매핑이 일관되게 유지된다. 비워진 꼬리
단위는 ALL_ACTIVE (0)으로 reset된다.
불변식 —
bit_area_length너머의 단위는 항상ALL_ACTIVE(0)이다. 강제 수단: 여기서의 후행memset, pre-zero된initialize,check_valid의 디버그 루프. 위반 시:set_bitarea_mvccid의 “길이를 올려 확장” 지름길이 stale 커밋 비트를 상속해, 보이지도 않은 MVCCID를 커밋된 것으로 표시한다.
8.11 cleanup_migrate_to_long_transations — 16개 유지, 나머지 evict
섹션 제목: “8.11 cleanup_migrate_to_long_transations — 16개 유지, 나머지 evict”// mvcc_active_tran::cleanup_migrate_to_long_transations -- src/transaction/mvcc_active_tran.cppconst size_t BITAREA_SIZE_AFTER_CLEANUP = 16;size_t delete_count = get_area_size () - BITAREA_SIZE_AFTER_CLEANUP;for (size_t i = 0; i < delete_count; i++) { bits = m_bit_area[i]; for (bit_pos = 0, mask = 1, long_tran_mvccid = get_mvccid (i * UNIT_BIT_COUNT); bit_pos < UNIT_BIT_COUNT && bits != ALL_COMMITTED; ++bit_pos, mask <<= 1, ++long_tran_mvccid) { if ((bits & mask) == 0) /* bit clear == still active */ { add_long_transaction (long_tran_mvccid); /* push straggler to long-tran array */ bits |= mask; /* set locally to allow early ALL_COMMITTED exit */ } } }ltrim_area (delete_count);가장 최근 16 단위를 유지하고, 더 오래된 get_area_size () - 16을 evict한다.
각 evict 단위에서, 모든 clear 비트 (여전히 활성인 MVCCID)는 long-tran 배열에
append된다. 비트를 로컬로 set하면 내부 루프가 단위가 ALL_COMMITTED로 읽히는
순간 short-circuit할 수 있다. 그 다음 ltrim_area (delete_count)가 마이그레이션된
단위를 떨어뜨린다. low-to-high 스캐닝이 append를 오름차순으로 만들어 §8.8 정렬
불변식을 만족한다.
8.12 check_valid — 디버그 불변식 게이트
섹션 제목: “8.12 check_valid — 디버그 불변식 게이트”// mvcc_active_tran::check_valid -- src/transaction/mvcc_active_tran.cpp (debug-only, #if !defined(NDEBUG))// 1. bits in the final partial unit, past bit_area_length, must be 0if ((m_bit_area_length % UNIT_BIT_COUNT) != 0) { size_t last_bit_pos = m_bit_area_length - 1; unit_type last_unit = *get_unit_of (last_bit_pos); for (size_t i = (last_bit_pos + 1); i < UNIT_BIT_COUNT; i++) if ((get_mask_of (i) & last_unit) != 0) { assert (false); } /* a set bit past the length is corruption */ }// 2. every unit fully past bit_area_length must equal ALL_ACTIVEfor (unit_type *p_area = get_unit_of (m_bit_area_length) + 1; p_area < m_bit_area + BITAREA_MAX_SIZE; ++p_area) if (*p_area != ALL_ACTIVE) { assert (false); }// 3. long-tran array is ascending and every entry precedes bit_area_start_mvccidfor (size_t i = 0; i < m_long_tran_mvccids_length; i++) { assert (MVCC_ID_PRECEDES (m_long_tran_mvccids[i], m_bit_area_start_mvccid)); assert (i == 0 || MVCC_ID_PRECEDES (m_long_tran_mvccids[i - 1], m_long_tran_mvccids[i])); }check_valid는 release 빌드에서 no-op이지만 (#if !defined (NDEBUG)), 모든
커밋 경로 mutation (set_bitarea_mvccid, ltrim_area,
cleanup_migrate_to_long_transations, remove_long_transaction, THREAD_SAFE
하의 copy_to) 뒤에 실행된다. 첫 조건은 m_bit_area_length가 단위 정렬되지
않을 때만 발화한다 — 마지막 부분 단위를 last_bit_pos + 1부터 UNIT_BIT_COUNT
까지 스캔하면서 각 비트가 클리어임을 assert한다. #2는 길이 너머의 전체 단위를
검사. #3은 베이스보다 엄격히 작은 정렬된 long-tran 배열. 위반이 있으면 디버그
에서 abort되어 유지 버그를 손상 지점에서 드러낸다.
8.13 챕터 요약 — 핵심 정리
섹션 제목: “8.13 챕터 요약 — 핵심 정리”-
read-only 커밋은 거의 공짜다. MVCCID가 없으면
logtb_complete_mvcc는complete_mvcc를 건너뛰고 가시성 슬롯만MVCCID_NULL로 reset한다. write 트랜잭션만 mutex-보호 경로를 탄다. -
publish는 단일, 마지막 store다.
next_trans_status_start가 슬롯의 버전을 stamp한다 (평이한 단조unsigned int. 세대당 +1. 정확한 동등성으로 recheck) — 페이로드가 존재하기 전에.next_tran_status_finish가 active-set을 복사하고 그 다음에야m_trans_status_history_position을 store한다 — 이것이 버전 recheck와 함께 새 스냅샷을 lock-free reader에게 원자적으로 가시화한다. -
커밋은 가시성 floor를 올리고, 롤백은 떨어뜨린다. 커밋은 per-tran 슬롯을
mvccid까지 위로 clamp (절대 내리지 않음) — 그래서 VACUUM이LOG_COMMIT이 영구 저장되기 전 데이터를 회수할 수 없다. 롤백은 즉시MVCCID_NULL로 설정. -
워터마크 전진은 lazy하고 단조다. 잠금 해제 후
complete_mvcc는 퇴역 MVCCID가 병목이었을 때만 oldest-active를 다시 계산하고, 슬롯 버전을 recheck 하고,advance_oldest_active가 CAS-bump한다. 워터마크는 오직 증가만 한다. -
퇴역은 윈도 위치로 라우팅되고 자체 압축한다.
set_inactive_mvccid는 sub-window MVCCID는remove_long_transaction으로, 나머지는set_bitarea_mvccid로 보낸다.set_bitarea_mvccid의 세 트리거가 윈도를 압축한다 —ltrim_area가 64 비트를 넘는 all-committed prefix를 떨어뜨리고,cleanup_migrate_to_long_transations가LONG_TRAN_THRESHOLD를 넘으면 16 단위를 남기고,BITAREA_MAX_BITS오버플로는 마이그레이션을 강제한다.check_valid(debug 전용)는 모든 mutation 뒤 깨끗한 꼬리 비트/단위와 베이스 아래의 정렬된 long-tran 배열을 assert한다.
Chapter 9: Vacuum 협조와 Oldest-Visible 워터마크
섹션 제목: “Chapter 9: Vacuum 협조와 Oldest-Visible 워터마크”MVCC는 어떤 live 스냅샷도 더 이상 필요로 하지 않을 때까지 오래된 버전을
유지한다. 그 결정은 하나의 전역 스칼라 — oldest-visible MVCCID 워터마크,
mvcctable::m_oldest_visible — 로 압축된다. 이 챕터는 모든 live 스냅샷에
걸쳐 그것이 어떻게 계산되는지, vacuum이 그것을 어떻게 소비해
VACUUM_RECORD_REMOVE를 구동하는지, 그리고 작은 MVCCID를 가진 길게 실행되는
writer 하나가 데이터베이스 전체의 회수를 어떻게 동결시키는지를 다룬다. 동반
문서 cubrid-mvcc.md는 vacuum이 왜 존재하는지와 master/worker 분리를
다룬다. 입력 (m_transaction_lowest_visible_mvccids[]이 MVCCID_ALL_VISIBLE로
시드됨)은 Ch.4/Ch.5에서, mvcc_satisfies_vacuum 술어는 Ch.7에서 왔다. 여기서
루프를 닫는다.
9.1 mvcctable 구조체 — 맥락 안의 워터마크 필드
섹션 제목: “9.1 mvcctable 구조체 — 맥락 안의 워터마크 필드”History ring 필드는 Ch.8에서 다루었다. 이 챕터의 워터마크 sub-state.
// class mvcctable (watermark fields) -- src/transaction/mvcc_table.hppusing lowest_active_mvccid_type = std::atomic<MVCCID>; // ... history-ring fields elided (Ch.8) ...lowest_active_mvccid_type *m_transaction_lowest_visible_mvccids; /* per-tran array */size_t m_transaction_lowest_visible_mvccids_size;lowest_active_mvccid_type m_current_status_lowest_active_mvccid; /* global floor */std::atomic<MVCCID> m_oldest_visible; /* cached watermark */std::atomic<size_t> m_ov_lock_count; /* >0 pins the watermark */| Field | Role | Why it exists |
|---|---|---|
m_transaction_lowest_visible_mvccids | 배열. tran index당 atomic<MVCCID> 하나. 각 슬롯은 그 트랜잭션의 스냅샷이 visible로 유지해야 할 가장 오래된 MVCCID | 최솟값에 대한 스냅샷별 입력. logtb_get_number_of_total_tran_indices()로 크기 설정 |
m_transaction_lowest_visible_mvccids_size | 캐시된 배열 길이 | sweep이 트랜잭션 테이블을 다시 조회하지 않고 반복 |
m_current_status_lowest_active_mvccid | 전역 floor. 현재 trans-status의 가장 낮은 활성 MVCCID. advance_oldest_active (Ch.8)에 의해 단조 전진 | 트랜잭션이 아직 자신의 per-tran 값을 publish하지 않았더라도 sweep을 시드하여 경계를 보장 |
m_oldest_visible | 캐시된 워터마크. update_global_oldest_visible이 store. get_global_oldest_visible이 read | vacuum 소비자당 atomic read 한 번. recompute를 master heartbeat로 amortize |
m_ov_lock_count | 워터마크를 pin한 작업 수. > 0은 캐시된 값이 전진해서는 안 됨을 의미 | get_global_oldest_visible()을 읽고 그것에 따라 작동하는 호출자가 작업이 끝날 때까지 floor를 안정되게 유지 |
m_current_trans_status / m_trans_status_history / m_trans_status_history_position / m_new_mvccid_lock / m_active_trans_mutex | 다른 챕터가 소유 — history ring과 status mutex (Ch.8), MVCCID 할당 락 (Ch.3) | 워터마크 상태가 아니다. lock-free sweep은 이것들을 읽지 않는다 |
flowchart TB FLOOR["m_current_status_lowest_active_mvccid\n(global floor, monotonic)"] --> COMPUTE["compute_oldest_visible_mvccid()"] ARR["m_transaction_lowest_visible_mvccids[0..N]\n(per-snapshot inputs)"] --> COMPUTE COMPUTE --> UPDATE["update_global_oldest_visible()"] LC["m_ov_lock_count\n(pin counter)"] --> UPDATE UPDATE --> OV["m_oldest_visible\n(cached watermark)"] OV --> GET["get_global_oldest_visible()"] --> VAC["vacuum consumers\n(threshold_mvccid)"]
Figure 9-1 — 워터마크 sub-state. 입력에서 vacuum이 읽는 캐시된 스칼라까지.
불변식 (워터마크 단조성): m_oldest_visible은 절대 감소하지 않는다.
update_global_oldest_visible이 store 전에
assert (m_oldest_visible.load () <= oldest_visible)로 강제한다. 회귀가 일어나면
더 오래된 threshold_mvccid를 가진 worker가, newer-but-lower 스냅샷이 여전히
필요로 하는 버전을 제거한다. 단조 floor m_current_status_lowest_active_mvccid가
계산된 최솟값이 비-감소가 되게 유지한다.
9.2 compute_oldest_visible_mvccid — 교차 스냅샷 sweep
섹션 제목: “9.2 compute_oldest_visible_mvccid — 교차 스냅샷 sweep”const. lock-free — atomic을 읽고 m_active_trans_mutex는 절대 잡지 않는다.
어떤 live 스냅샷도 여전히 볼 수 있는 최소 MVCCID를 반환한다.
// mvcctable::compute_oldest_visible_mvccid -- src/transaction/mvcc_table.cppcubmem::appendable_array<size_t, 32> waiting_mvccids_pos;MVCCID lowest_active_mvccid = oldest_active_get (m_current_status_lowest_active_mvccid, 0, /*...*/); /* <- seed = floor */for (size_t idx = 0; idx < m_transaction_lowest_visible_mvccids_size; idx++) { MVCCID loaded = oldest_active_get (m_transaction_lowest_visible_mvccids[idx], idx, /*...*/); if (loaded == MVCCID_ALL_VISIBLE) waiting_mvccids_pos.append (idx); /* <- in flight; defer (9.2.1) */ else if (loaded != MVCCID_NULL && MVCC_ID_PRECEDES (loaded, lowest_active_mvccid)) lowest_active_mvccid = loaded; /* <- min; NULL = ended, ignored */ }// ... re-check loop for deferred slots (9.2.1) ...assert (MVCCID_IS_NORMAL (lowest_active_mvccid)); /* return value */Sweep은 각 슬롯을 Ch.4/Ch.5가 거기 쓴 세 sentinel 경우로 분류한다 —
MVCCID_ALL_VISIBLE (== 3)은 build_mvcc_info가 중간 단계에 있다는 뜻이다
(슬롯은 claim되었지만 실제 값은 아직 publish 안 됨) — index를 defer하고
re-check. MVCCID_NULL (== 0)은 트랜잭션이 끝났다는 뜻이다
(reset_transaction_lowest_active가 씀, 9.5) — 무시. >= MVCCID_FIRST는
모두 publish된 값 — MVCC_ID_PRECEDES를 통해 최솟값을 잡는다.
9.2.1 deferred re-check 루프와 20-retry 백오프
섹션 제목: “9.2.1 deferred re-check 루프와 20-retry 백오프”MVCCID_ALL_VISIBLE은 일시적이다 (Ch.5 — stamp, floor 읽기, 덮어쓰기). 따라서
루프는 deferred 슬롯이 실제 값을 publish하거나 MVCCID_NULL로 떨어질 때까지
spin한다.
// mvcctable::compute_oldest_visible_mvccid (re-check loop) -- src/transaction/mvcc_table.cppsize_t retry_count = 0;while (waiting_mvccids_pos.get_size () > 0) { if (++retry_count % 20 == 0) { thread_sleep (10); } /* <- 10ms backoff every 20 spins */ for (size_t i = waiting_mvccids_pos.get_size () - 1; i < waiting_mvccids_pos.get_size (); --i) { /* reverse walk: decrement past 0 wraps >= size, so erase shrinks the tail safely */ size_t pos = waiting_mvccids_pos.get_array ()[i]; MVCCID loaded = oldest_active_get (m_transaction_lowest_visible_mvccids[pos], pos, /*...*/); if (loaded == MVCCID_ALL_VISIBLE) { continue; } /* <- still unset; keep in set */ if (loaded != MVCCID_NULL && MVCC_ID_PRECEDES (loaded, lowest_active_mvccid)) lowest_active_mvccid = loaded; waiting_mvccids_pos.erase (i); /* <- resolved (value or NULL); drop */ } }여전히 MVCCID_ALL_VISIBLE을 보이는 re-read는 슬롯을 유지한다 (continue).
다른 값은 해결된 것이다 — 현재 최솟값보다 작은 정상 MVCCID는 최솟값을 낮추고,
그 외는 erase로 떨어뜨린다.
불변식 (sweep은 정상 결과로 종료한다): assert (MVCCID_IS_NORMAL (...))로
끝난다. 시드는 항상 >= MVCCID_FIRST이고, 모든 MVCCID_ALL_VISIBLE 슬롯은
해결된다 (publish 중인 writer가 두 라인을 back-to-back으로 실행하므로, Ch.5).
올바른 작동에서는 루프가 hang할 수 없다.
flowchart TD
A["seed = m_current_status_lowest_active_mvccid"] --> B["sweep idx 0..size"]
B --> C{슬롯 값?}
C -->|ALL_VISIBLE| D["defer: append idx"]
C -->|NULL| E["무시"]
C -->|normal| F["if PRECEDES min: min = slot"]
D --> G{대기 set 비었나?}
E --> G
F --> G
G -->|yes| Z["assert NORMAL; return min"]
G -->|no| H["retry++; 20마다 10ms sleep"]
H --> I["대기 set reverse-walk"] --> J{슬롯 re-read}
J -->|여전히 ALL_VISIBLE| K["유지"] --> G
J -->|normal/NULL| L["min 갱신 가능; erase"] --> G
Figure 9-2 — compute_oldest_visible_mvccid 제어 흐름, 모든 분기.
9.3 update_global_oldest_visible — pin된 더블 체크 store
섹션 제목: “9.3 update_global_oldest_visible — pin된 더블 체크 store”master heartbeat (9.6)은 워터마크를 pin하는 작업이 없을 때만 recompute한다.
m_ov_lock_count는 두 번 체크된다 — 계산 전과, sweep 후, store 전.
// mvcctable::update_global_oldest_visible -- src/transaction/mvcc_table.cppMVCCID mvcctable::update_global_oldest_visible (){ if (m_ov_lock_count == 0) /* <- gate 1: skip work if pinned */ { MVCCID oldest_visible = compute_oldest_visible_mvccid (); if (m_ov_lock_count == 0) /* <- gate 2: pin may have arrived during sweep */ { assert (m_oldest_visible.load () <= oldest_visible); /* monotonicity (9.1) */ m_oldest_visible.store (oldest_visible); } } return m_oldest_visible.load (); /* <- always return cached (possibly stale) */}세 가지 결과 — gate 1에서 pin이면 sweep을 건너뛰고 캐시된 값 반환. sweep
중에 도착한 pin (gate 2 != 0)은 fresh 값을 폐기. == 0이 두 gate 모두에서
만 단조성을 assert하고 store한다. Gate 2가 중요한 이유는 sweep이 밀리초가
걸릴 수 있고, 평이한 atomic load면 충분하기 때문이다 — pinning 호출자가
워터마크를 읽기 전에 m_ov_lock_count를 올리므로 happens-before 엣지는
호출자 쪽에 있다.
9.4 pin API — lock / unlock / is_locked / get
섹션 제목: “9.4 pin API — lock / unlock / is_locked / get”모두 평범한 atomic.
// pin API -- src/transaction/mvcc_table.cppMVCCID mvcctable::get_global_oldest_visible () const { return m_oldest_visible.load (); }void mvcctable::lock_global_oldest_visible () { ++m_ov_lock_count; }void mvcctable::unlock_global_oldest_visible () { assert (m_ov_lock_count > 0); --m_ov_lock_count; }bool mvcctable::is_global_oldest_visible_locked () const { return m_ov_lock_count != 0; }get_global_oldest_visible은 vacuum의 빠른 경로다. lock/unlock은 pin 쌍.
is_..._locked는 미해결 pin이 있는지 보고한다.
불변식 (균형 잡힌 pin): 모든 lock은 정확히 하나의 unlock과 짝지어진다.
unlock은 m_ov_lock_count > 0을 assert하여 double-unlock이나 missing-lock에서
발화한다. log_tdes 래퍼가 함수 경계 너머로 쌍을 운반한다 — unlock은
log_complete (log_manager.c)에 있고, 매칭하는 lock은 locator 쪽
(locator_sr.c, 예 xlocator_upgrade_instances_domain에서 heap_vacuum_all_objects
바로 전, 그리고 redistribute_partition_data)에서 잡혀 그 작업이
get_global_oldest_visible()을 읽는 동안 워터마크가 pin된 상태로 유지된다.
누수된 pin은 m_oldest_visible을 영원히 동결시키고 회수를 멈춘다.
9.5 reset_transaction_lowest_active — 트랜잭션 끝에서 슬롯 비우기
섹션 제목: “9.5 reset_transaction_lowest_active — 트랜잭션 끝에서 슬롯 비우기”끝난 트랜잭션의 슬롯은 MVCCID_NULL로 돌아가야 한다. 이것은 커밋 경로에서
per-tran 배열에 MVCCID_NULL을 쓰는 유일한 writer다.
// mvcctable::reset_transaction_lowest_active -- src/transaction/mvcc_table.cppvoid mvcctable::reset_transaction_lowest_active (int tran_index){ oldest_active_set (m_transaction_lowest_visible_mvccids[tran_index], tran_index, MVCCID_NULL, oldest_active_event::RESET);}LOG_COMMIT에 대한 순서가 pin (9.4)의 이유다. log_complete은 순서대로 한다 —
commit/abort 레코드 append → pin 떨어뜨림 → 그다음, 커밋이면 슬롯 reset.
// log_complete (commit tail) -- src/transaction/log_manager.clog_append_commit_log (thread_p, tdes, &commit_lsa);/* ... */tdes->unlock_global_oldest_visible_mvccid (); /* <- drop the pin first */if (iscommitted == LOG_COMMIT) log_Gl.mvcc_table.reset_transaction_lowest_active (LOG_FIND_THREAD_TRAN_INDEX (thread_p));complete_mvcc (Ch.8)는 이미 슬롯을 이 트랜잭션 자신의 mvccid로 설정했고,
pin이 워터마크를 안정되게 유지했다. LOG_COMMIT이 append된 뒤에야 pin이 떨어
지고 슬롯이 reset된다. 더 일찍 reset하면 vacuum이 post-crash 복구가 존재할
것으로 기대하는 modification을 정리할 수 있다.
9.6 vacuum 쪽 — master heartbeat과 record당 소비
섹션 제목: “9.6 vacuum 쪽 — master heartbeat과 record당 소비”매 vacuum_master_task::execute마다 master는 워터마크를 refresh하고 capture
한다. vacuum_boot (“for debug only”)와 vacuum_data_load_and_recover에서도
실행되므로 fresh 서버는 항상 첫 job 전에 워터마크를 가진다.
// vacuum_master_task::execute -- src/query/vacuum.cm_oldest_visible_mvccid = log_Gl.mvcc_table.update_global_oldest_visible ();master는 블록 적격성을 이걸로 게이트한다.
// vacuum_master_task::is_cursor_entry_ready_to_vacuum -- src/query/vacuum.cif (m_cursor.get_current_entry ().newest_mvccid >= m_oldest_visible_mvccid) return false; /* <- newest still visible; whole block not vacuumable */블록은 blockid 순서로 스캔되고 현재 블록이 ready가 아니면 나중 블록도 ready일
수 없으므로 master는 첫 ready-아닌 블록에서 break한다.
9.6.1 vacuum_process_log_block — job당 threshold 캡처
섹션 제목: “9.6.1 vacuum_process_log_block — job당 threshold 캡처”각 worker가 워터마크를 로컬 threshold_mvccid로 다시 읽고, NDEBUG tripwire가
모든 op를 세 방향으로 묶는다.
// vacuum_process_log_block -- src/query/vacuum.cMVCCID threshold_mvccid = log_Gl.mvcc_table.get_global_oldest_visible (); /* <- one atomic load */#if !defined (NDEBUG)if (MVCC_ID_FOLLOW_OR_EQUAL (mvccid, threshold_mvccid) /* not yet below watermark? */ || MVCC_ID_PRECEDES (mvccid, data->oldest_visible_mvccid) /* older than block floor? */ || MVCC_ID_PRECEDES (data->newest_mvccid, mvccid)) /* newer than block ceiling? */ { assert (0); logpb_fatal_error (thread_p, true, ARG_FILE_LINE, "vacuum_process_log_block"); goto end; }#endifVACUUM_DATA_ENTRY::oldest_visible_mvccid (log 시점에 캡처, 9.6.3)는 job을
아래에서, live threshold_mvccid는 위에서 묶는다. 현재 워터마크 이상의 op는
job에 도달해서는 안 된다 — master gate가 그 블록을 defer했을 것이기 때문이다 —
그래서 assert다. 수집된 힙 객체는 vacuum_heap_page로 가고, 그것이
threshold_mvccid를 record 술어로 운반한다.
9.6.2 mvcc_satisfies_vacuum — record당 판정
섹션 제목: “9.6.2 mvcc_satisfies_vacuum — record당 판정”vacuum_heap_page은 MVCCID_IS_NORMAL (threshold_mvccid)를 assert하고, 각
후보 record에 대해 Ch.7 술어의 판정으로 dispatch한다.
// vacuum_heap_page (per-record) -- src/query/vacuum.chelper.can_vacuum = mvcc_satisfies_vacuum (thread_p, &helper.mvcc_header, threshold_mvccid);if (helper.can_vacuum == VACUUM_RECORD_REMOVE) vacuum_heap_record (thread_p, &helper); /* <- whole version dies */else if (helper.can_vacuum == VACUUM_RECORD_DELETE_INSID_PREV_VER) vacuum_heap_record_insid_and_prev_version (thread_p, &helper); /* <- shrink header *//* else VACUUM_RECORD_CANNOT_VACUUM: leave it */술어 (본문은 Ch.7에서 해부)는 oldest_mvccid를 받는데 — 여기서 그것은
워터마크다 — 따라서 m_oldest_visible 하나가 record header에 대한 각 판정을
혼자 결정한다.
| 워터마크 대비 레코드 상태 | 판정 | 효과 |
|---|---|---|
Deleter 커밋됨 그리고 delete MVCCID < 워터마크 | VACUUM_RECORD_REMOVE | 전체 제거 |
삭제 안 됨 (또는 >= 워터마크로 삭제) 그리고 insert all-visible / < 워터마크 | VACUUM_RECORD_DELETE_INSID_PREV_VER | 버전 유지. insert MVCCID + prev-version LSA 트림 |
>= 워터마크로 insert됨, 또는 insert가 all-visible 아님 | VACUUM_RECORD_CANNOT_VACUUM | live 스냅샷이 필요할 수 있음. 그대로 둠 |
술어는 vacuum_heap_page의 record당 job 경로에서 호출된다. 다른 두 sites가
존재한다 — is_not_vacuumed_and_lost (vacuum_Data.oldest_unvacuumed_mvccid에
대한 consistency check)와 vacuum_rv_check_at_undo (get_global_oldest_visible()에
대한 undo 시점 recheck) — 하지만 어느 쪽도 블록-job hot path가 아니다. 셋 모두
같은 단조 워터마크 lineage를 따른다.
9.6.3 log 시점의 블록 레벨 워터마크 캡처
섹션 제목: “9.6.3 log 시점의 블록 레벨 워터마크 캡처”VACUUM_DATA_ENTRY는 블록이 log된 시점의 워터마크를 기록한다. 그래서 job은
전역 워터마크가 전진해도 그 floor를 유지한다.
// struct vacuum_data_entry -- src/query/vacuum.cstruct vacuum_data_entry { VACUUM_LOG_BLOCKID blockid; LOG_LSA start_lsa; // lsa of last mvcc op log record in block MVCCID oldest_visible_mvccid; // oldest visible MVCCID while block was logged MVCCID newest_mvccid; // newest MVCCID in log block // ...};append 시 oldest_visible_mvccid는 <= get_global_oldest_visible()이며 이전
블록 값 >=임이 assert된다 — 블록 스트림에 투영된 9.1 단조성 불변식이다.
9.7 구조적 한계 — 작은 writer 하나가 모든 것을 pin한다
섹션 제목: “9.7 구조적 한계 — 작은 writer 하나가 모든 것을 pin한다”워터마크는 전역 최솟값이며, 가장 오래된 live 스냅샷만큼만 fresh하다.
작은 MVCCID를 잡고 있는 idle T_old는 자기 슬롯을 작게 유지하므로 모든
sweep이 그것을 최솟값으로 가져가고 m_oldest_visible은 동결된다.
flowchart LR TOLD["T_old (작은 MVCCID, idle)"] -->|슬롯이 작게 유지| ARR["per-tran 배열 min"] ARR --> WM["m_oldest_visible 동결"] WM --> PRED["mvcc_satisfies_vacuum -> CANNOT_VACUUM"] PRED --> ACC["죽은 버전 누적"]
Figure 9-3 — 단일 길게 실행되는 writer가 전역 워터마크를 pin하고 데이터베이스 전체의 회수를 정지시킨다.
이는 per-table이나 per-tablespace scope이 없는 단일 워터마크에 본질적이다 —
가장 느린 스냅샷이 시스템을 지배한다. 다른 곳의 long transaction이
autovacuum/oldest xmin을 블록하는 것의 MVCC 버전이다. 해결책은 운영
차원이다 (트랜잭션 lifetime 제한, idle-in-transaction 회피). m_ov_lock_count
pin (9.3/9.4)은 동일한 동결의 의도된 경계가 있는 버전이며, 하나의 pin된
작업 윈도에 scope된다.
9.8 챕터 요약 — 핵심 정리
섹션 제목: “9.8 챕터 요약 — 핵심 정리”- 워터마크는 하나의 스칼라
m_oldest_visible이다 — 어떤 live 스냅샷이든 볼 수 있는 최소 MVCCID. vacuum은get_global_oldest_visible로 읽고, master heartbeat에서 recompute된다. compute_oldest_visible_mvccid는 per-tran 배열을 lock-free로 sweep한다. 단조 floor로 시드.MVCCID_ALL_VISIBLE→ defer,MVCCID_NULL→ 무시, normal → min. deferred 슬롯은 값을 publish할 때까지 re-check (20 spin마다 10 ms 백오프), 그다음 정상 결과를 assert.update_global_oldest_visible은m_ov_lock_count를 더블 체크한다 (sweep 전, store 전). 따라서 sweep 중간의 pin이 새 값을 폐기한다. store는 단조성을 assert.- pin은 pin된 작업 윈도에 걸쳐 워터마크를 동결한다. lock은 locator 쪽에서
잡히고, unlock은
log_complete에서 일어나며, 그 뒤에 슬롯이reset_transaction_lowest_active로MVCCID_NULL로 reset된다. - vacuum은 워터마크를 두 번 소비한다. master 블록 gate (
newest_mvccid >= m_oldest_visible_mvccid→ 건너뜀)와 workerthreshold_mvccid가mvcc_satisfies_vacuum으로 들어간다. 그 술어 혼자가REMOVE/DELETE_INSID_PREV_VER/CANNOT_VACUUM을 선택. - 작은 MVCCID 길게 실행되는 writer 하나가 워터마크를 pin하고 데이터베이스 전체의 회수를 정지시킨다 — per-object scoping이 없는 단일 전역 최솟값의 비용. 해결책은 운영 차원이다.
Chapter 10: Sub-Transaction과 특수 경로
섹션 제목: “Chapter 10: Sub-Transaction과 특수 경로”Chapter 3부터 9까지는 깔끔한 생애주기를 추적했다 — 하나의 트랜잭션이 하나의
MVCCID를 받고, 레코드에 도장을 찍고, 커밋에서 가시화되고, vacuum이 회수한다.
이 챕터는 그 모델에 맞지 않는 경로들을 다룬다 — 부모가 여전히 열려 있는
동안 자신만의 MVCCID가 필요하고 그 부모보다 먼저 완료되어야 하는
sub-transaction (savepoint / system operation), 레코드에 MVCC 헤더 필드가
없는 MVCC-disabled 클래스 (root class, _db_serial, collation/HA cached-OID
클래스), reset_start_mvccid를 통한 재시작 시드, 그리고 Chapter 5에서
미뤄둔 2048-ring saturation 질문.
이 경로들이 perturb하는 스냅샷/가시성 이론은 cubrid-mvcc.md를 참고하라.
sub-transaction 경계 vs lock escalation과 SERIALIZABLE write-skew에 대해서는
§10.8을 참고하라.
10.1 두 구조체, 완성
섹션 제목: “10.1 두 구조체, 완성”mvcc_info (트랜잭션별 상태, Ch.1/Ch.3)와 mvcc_trans_status (하나의 ring
슬롯, Ch.1/Ch.8)에 대해 sub-transaction 경로만 사용하는 필드를 다루며 마무리
한다.
// struct mvcc_info -- src/transaction/mvcc.hstruct mvcc_info{ MVCC_SNAPSHOT snapshot; /* MVCC Snapshot */ MVCCID id; /* the transaction's own MVCCID (Ch.3) */ MVCCID recent_snapshot_lowest_active_mvccid; /* fast-reject floor (Ch.4) */ std::vector<MVCCID> sub_ids; /* MVCC sub-transaction ID array */ bool is_sub_active; /* true while a sub-transaction is running */ // ... methods condensed ...};| Field | Role | Why it exists |
|---|---|---|
snapshot | 읽기 뷰 (Ch.5에서 빌드, Ch.6에서 소비) | worker당 consistency 윈도당 스냅샷 한 개 |
id | 트랜잭션의 자신의 MVCCID, lazy 할당 (Ch.3) | top-level 쓰기에 의해 record header에 쓰이는 도장 |
recent_snapshot_lowest_active_mvccid | 어떤 MVCCID도 확실히 비활성인 floor — mvcc_is_active_id의 fast-reject gate (Ch.4) | 오래된 ID에 대한 전역 탐색을 회피 |
sub_ids | 중첩된 sub-transaction을 위한 MVCCID LIFO 스택. 가장 새 것이 back() | 부모가 자기 생애 동안 여러 MVCCID를 필요로 할 수 있다 — 열린 시스템 op마다 하나. 역순으로 완료되어야 함 |
is_sub_active | sub-transaction이 “현재” 쓰기 정체성을 소유하는 동안 true 설정 | 활성 쓰기 MVCCID가 id가 아니라 sub_ids.back()임을 신호. copy_to에서 미러링되지만 MVCC core에서는 절대 읽히지 않음 — passive server를 위한 정보 상태 |
sub_ids는 set이 아니라 스택이다. logtb_assign_subtransaction_mvccid는
push_back만 한다. logtb_complete_sub_mvcc는 back()에서 읽은 값만
pop_back한다. out-of-order 완료는 잘못된 id를 pop한다. CUBRID의 system
operation/savepoint 기계가 요구되는 LIFO 중첩을 보장한다.
// struct mvcc_trans_status -- src/transaction/mvcc_table.hppstruct mvcc_trans_status{ enum event_type { COMMIT, ROLLBACK, SUBTRAN }; mvcc_active_tran m_active_mvccs; /* the bit-area + long-list snapshot */ MVCCID m_last_completed_mvccid; // just for info event_type m_event_type; // just for info std::atomic<version_type> m_version; // ... methods condensed ...};| Field | Role | Why it exists |
|---|---|---|
m_active_mvccs | 이 슬롯에 snapshot된 active-set (bit-area + long-list) | 스냅샷 빌더가 복사하는 페이로드 (Ch.4/Ch.5) |
m_last_completed_mvccid | 이 슬롯을 만든 완료의 MVCCID. 진단용 | 디버그/트레이스 보조. 가시성은 읽지 않음 |
m_event_type | 완료를 태깅 — COMMIT, ROLLBACK, 또는 SUBTRAN | 진단용. sub-completion을 top-level과 구분 |
m_version | 모든 status transition마다 증가하는 단조 카운터. 스냅샷 빌더가 mid-copy mutation을 감지하기 위해 다시 읽음 (Ch.5 재시도 루프) | reader가 mutex 없이 복사할 수 있게 하는 lock-free consistency check |
m_event_type의 역할 행렬:
| 생성 호출 | m_event_type | oldest 전진? |
|---|---|---|
complete_mvcc(.., committed=true) | COMMIT | 예 — 커밋된 작업이 floor를 올릴 수 있음 |
complete_mvcc(.., committed=false) | ROLLBACK | 예 — 롤백된 ID가 active set을 떠남 |
complete_sub_mvcc | SUBTRAN 의도. 실제로는 손대지 않음 (§10.5 버그) | 아니오 — 부모가 여전히 열려 있어 sub-id는 절대 lowest일 수 없음 |
10.2 sub-transaction MVCCID 할당
섹션 제목: “10.2 sub-transaction MVCCID 할당”logtb_get_new_subtransaction_mvccid는 system operation이나 savepoint가
자신의 정체성으로 써야 할 때의 진입점이다.
// logtb_get_new_subtransaction_mvccid -- src/transaction/log_tran_table.cvoidlogtb_get_new_subtransaction_mvccid (THREAD_ENTRY * thread_p, MVCC_INFO * curr_mvcc_info){ MVCCID mvcc_subid; mvcctable *mvcc_table = &log_Gl.mvcc_table;
if (MVCCID_IS_VALID (curr_mvcc_info->id)) { mvcc_subid = mvcc_table->get_new_mvccid (); /* parent already has an id */ } else { mvcc_table->get_two_new_mvccid (curr_mvcc_info->id, mvcc_subid); /* seed parent + sub */ } logtb_assign_subtransaction_mvccid (thread_p, curr_mvcc_info, mvcc_subid);}부모가 이미 MVCCID를 가지고 있는지에 따라 두 분기다. 부모 id 유효 →
get_new_mvccid로 하나 할당. 부모 id NULL → 부모가 한 번도 쓰지 않았다
(Ch.3의 lazy 할당이 발화한 적 없음). 가시성 (Ch.6)이 부모의 id가 자신의 sub
앞에 와야 한다고 요구하므로, get_two_new_mvccid가 단일 락 아래에서 두
연속 id를 끌어온다 — 부모에게 첫 번째 (참조로), sub에게 두 번째.
// mvcctable::get_two_new_mvccid -- src/transaction/mvcc_table.cppvoidmvcctable::get_two_new_mvccid (MVCCID &first, MVCCID &second){ m_new_mvccid_lock.lock (); first = log_Gl.hdr.mvcc_next_id; MVCCID_FORWARD (log_Gl.hdr.mvcc_next_id); second = log_Gl.hdr.mvcc_next_id; MVCCID_FORWARD (log_Gl.hdr.mvcc_next_id); m_new_mvccid_lock.unlock ();}불변식 — 부모 id가 모든 sub-id에 엄격히 선행한다. 어느 분기든 부모를 sub
아래로 유지하며 (이후 카운터, 또는 first vs second = first + 1), 두 id
모두 하나의 m_new_mvccid_lock 아래에서 잡혀 사이에 끼어드는 것이 없다.
가시성은 이것에 의존해 sub-stamped 레코드가 parent-stamped 레코드보다 “더
새것”으로 읽히게 한다.
flowchart TD
A["logtb_get_new_subtransaction_mvccid"] --> B{"MVCCID_IS_VALID(curr_mvcc_info->id)?"}
B -- "yes (부모 stamped)" --> C["get_new_mvccid() -> mvcc_subid"]
B -- "no (부모 unstamped)" --> D["get_two_new_mvccid(id, mvcc_subid)\nid := first, mvcc_subid := second"]
C --> E["logtb_assign_subtransaction_mvccid"]
D --> E
E --> F["sub_ids.push_back(mvcc_subid)"]
Figure 10-1: sub-transaction MVCCID 할당의 분기 구조.
logtb_assign_subtransaction_mvccid는 load-bearing 단언을 운반한다.
// logtb_assign_subtransaction_mvccid -- src/transaction/log_tran_table.cstatic voidlogtb_assign_subtransaction_mvccid (THREAD_ENTRY * thread_p, MVCC_INFO * curr_mvcc_info, MVCCID mvcc_subid){ assert (MVCCID_IS_VALID (curr_mvcc_info->id)); /* <- parent MUST be stamped by now */ curr_mvcc_info->sub_ids.push_back (mvcc_subid);}push할 시점에는 부모의 id가 유효하다 (입구에서 유효했거나, 방금
get_two_new_mvccid가 설정). unstamped 부모에 push하는 것은 버그며, 디버그
빌드에서 여기서 잡힌다.
10.3 부모가 자신의 sub-transaction 쓰기를 본다
섹션 제목: “10.3 부모가 자신의 sub-transaction 쓰기를 본다”sub-stamped 레코드는 부모에게 (그리고 그것의 더 새 sub들에게) “내가 쓴
것”으로 다시 읽혀야지, 외부의 활성 작업으로 읽혀서는 안 된다 —
logtb_is_current_mvccid의 일이다. Chapter 6의
MVCC_IS_REC_INSERTED_BY_ME / MVCC_IS_REC_DELETED_BY_ME 매크로를 통해 도달
한다.
// logtb_is_current_mvccid -- src/transaction/log_tran_table.cboollogtb_is_current_mvccid (THREAD_ENTRY * thread_p, MVCCID mvccid){ // ... condensed: tdes lookup + assert ... MVCC_INFO *curr_mvcc_info = &tdes->mvccinfo; if (curr_mvcc_info->id == mvccid) { return true; /* the parent's own id */ } else if (curr_mvcc_info->sub_ids.size () > 0) { for (size_t i = 0; i < curr_mvcc_info->sub_ids.size (); i++) { if (curr_mvcc_info->sub_ids[i] == mvccid) { return true; /* one of my sub-transactions */ } } } return false;}모든 종료 — (1) id == mvccid → true, 부모의 top-level 쓰기. (2)
sub_ids가 비어 있지 않으면 벡터 전체를 선형 스캔 (i < size(),
back()만이 아니다) — 중첩된 system op는 여러 sub-id를 스택할 수 있고, 더
이른 여전히 열려 있는 sub의 레코드도 “내 것”으로 카운트되어야 한다. (3)
빈 또는 매치 없음 → false, 스냅샷 기반 active check로 폴스루.
MVCC_IS_REC_INSERTED_BY_ME은 곧장
logtb_is_current_mvccid (thread_p, rec_header->mvcc_ins_id)로 확장된다.
동반 함수 logtb_find_current_mvccid / logtb_get_current_mvccid는 다른 쪽에서
write 정체성을 해결한다 — 비어 있지 않으면 sub_ids.back() (가장 안쪽 열린
sub), 아니면 id. 따라서 sub가 열린 상태에서의 쓰기는 sub-id로 stamp되고,
logtb_is_current_mvccid가 그것이 “내 것”으로 다시 읽힘을 보장한다.
mvcc_is_active_id (Ch.4)는 그 위에 fast-reject floor를 한 층 둔다.
// mvcc_is_active_id -- src/transaction/mvcc.cSTATIC_INLINE boolmvcc_is_active_id (THREAD_ENTRY * thread_p, MVCCID mvccid){ // ... condensed: tdes lookup + assert ... MVCC_INFO *curr_mvcc_info = &tdes->mvccinfo; if (MVCC_ID_PRECEDES (mvccid, curr_mvcc_info->recent_snapshot_lowest_active_mvccid)) { return false; /* below the floor: definitely inactive */ } if (logtb_is_current_mvccid (thread_p, mvccid)) { return true; /* mine (parent or any sub) */ } return log_Gl.mvcc_table.is_active (mvccid); /* foreign: global probe */}stateDiagram-v2 [*] --> CheckFloor CheckFloor --> Inactive: mvccid precedes recent_lowest CheckFloor --> CheckMine: at or above floor CheckMine --> Active: id or any sub_id matches CheckMine --> GlobalProbe: no local match GlobalProbe --> Active: mvcc_table.is_active true GlobalProbe --> Inactive: not in active set
Figure 10-2: mvcc_is_active_id — 로컬 sub_ids 체크가 값싼 floor reject와
비싼 전역 탐색 사이에 있다.
10.4 sub-transaction 완료
섹션 제목: “10.4 sub-transaction 완료”sub는 부모 전에 끝난다. logtb_complete_sub_mvcc가 트랜잭션별 절반을 실행한
뒤 부모의 live 스냅샷을 패치한다.
// logtb_complete_sub_mvcc -- src/transaction/log_tran_table.cvoidlogtb_complete_sub_mvcc (THREAD_ENTRY * thread_p, LOG_TDES * tdes){ MVCC_INFO *curr_mvcc_info = &tdes->mvccinfo; MVCCID mvcc_sub_id = curr_mvcc_info->sub_ids.back (); /* innermost open sub */
mvcc_table->complete_sub_mvcc (mvcc_sub_id); /* global half */ curr_mvcc_info->sub_ids.pop_back (); /* drop it from the stack */
if (tdes->mvccinfo.snapshot.valid) { MVCC_SNAPSHOT *snapshot = &tdes->mvccinfo.snapshot; if (mvcc_sub_id >= snapshot->highest_completed_mvccid) { snapshot->highest_completed_mvccid = mvcc_sub_id; MVCCID_FORWARD (snapshot->highest_completed_mvccid); } snapshot->m_active_mvccs.set_inactive_mvccid (mvcc_sub_id); }}분기 — (1) sub_ids.back()을 읽고 (LIFO §10.1), 전역
complete_sub_mvcc를 호출 (§10.5), 그다음 pop_back. pop 후
logtb_is_current_mvccid는 새 읽기에 대해 sub-id를 더 이상 매치하지 않으므로,
fix-up은 기존 스냅샷을 고쳐야 한다. (2) 유효 스냅샷 →
mvcc_sub_id >= highest_completed_mvccid이면 ceiling을 sub-id 너머 한 칸으로
올림. 그다음 무조건적으로 active bit-area에서 클리어 (set_inactive_mvccid).
(3) 유효 스냅샷 없음 (READ COMMITTED의 문장 사이, 또는 아직 없음) →
건너뜀. 다음 build_mvcc_info가 1단계에서 갱신된 전역 상태를 가져간다.
불변식: 부모의 스냅샷은 자신의 커밋된 sub-transaction을 절대 시야에서 놓치지 않는다. sub-id는 스냅샷의 ceiling 이후에 할당되었으므로, 패치되지 않은 스냅샷은 그것을 “too new”로 판정한다. ceiling 인상과 active-set 클리어가 Ch.6 술어의 양쪽을 고쳐 부모가 자신의 sub의 행을 즉시 읽게 한다.
10.5 전역 ring의 SUBTRAN 이벤트
섹션 제목: “10.5 전역 ring의 SUBTRAN 이벤트”mvcctable::complete_sub_mvcc는 전역 대응물 — complete_mvcc (Ch.8)과 거의
동일하지만 oldest-active recompute를 생략한다.
// mvcctable::complete_sub_mvcc -- src/transaction/mvcc_table.cppvoidmvcctable::complete_sub_mvcc (MVCCID mvccid){ assert (MVCCID_IS_VALID (mvccid)); std::unique_lock<std::mutex> ulock (m_active_trans_mutex); /* only one status change at a time */
mvcc_trans_status::version_type next_version; size_t next_index; mvcc_trans_status &next_status = next_trans_status_start (next_version, next_index);
// update current trans status m_current_trans_status.m_active_mvccs.set_inactive_mvccid (mvccid); m_current_trans_status.m_last_completed_mvccid = mvccid; m_current_trans_status.m_last_completed_mvccid = mvcc_trans_status::SUBTRAN; /* source-as-is; see note */
next_tran_status_finish (next_status, next_index); /* publish new ring slot */ ulock.unlock (); // mvccid can't be lowest, so no need to update it here}Walkthrough — (1) m_active_trans_mutex 잠금. (2) next_trans_status_start가
m_version을 증가시키고 다음 슬롯을 예약+무효화 (Ch.8의 버전 프로토콜 —
증가가 concurrent 스냅샷 복사를 재시도하게 만든다). (3) 현재 status active-set
에서 sub-id 클리어. (4) info 필드 기록 후 next_tran_status_finish로 publish
(active-set을 예약된 슬롯으로 복사, m_trans_status_history_position 전진).
(5) advance_oldest_active 없음 — 주석이 말한다 — 열린 부모는 더 오래된
id를 들고 있으므로 sub-id는 절대 oldest-visible 워터마크일 수 없다 (Ch.9).
m_last_completed_mvccid에 대한 이중 할당은 complete_mvcc에서의 copy-paste
실수다. 두 필드 모두 // just for info이므로 무해하다 (Open Question #2).
flowchart TD A["complete_sub_mvcc(mvccid)"] --> B["m_active_trans_mutex 잠금"] B --> C["next_trans_status_start\nm_version 증가, 슬롯 예약"] C --> D["m_current.set_inactive_mvccid(mvccid)"] D --> E["info 필드 기록"] E --> F["next_tran_status_finish\nactive-set 복사, position 전진"] F --> G["잠금 해제"] G --> H["return — advance_oldest_active 없음"]
Figure 10-3: complete_sub_mvcc 흐름 — 부재하는 oldest-active recompute에 주목.
10.6 MVCC-disabled 클래스
섹션 제목: “10.6 MVCC-disabled 클래스”mvcc_is_mvcc_disabled_class는 순전히 class OID로부터 참여를 결정한다.
// mvcc_is_mvcc_disabled_class -- src/transaction/mvcc.cboolmvcc_is_mvcc_disabled_class (const OID * class_oid){ if (OID_ISNULL (class_oid) || OID_IS_ROOTOID (class_oid)) { return true; /* root class (the class-of-classes) */ } if (oid_is_serial (class_oid)) { return true; /* _db_serial: serial/auto-increment generators */ } if (oid_check_cached_class_oid (OID_CACHE_COLLATION_CLASS_ID, class_oid)) { return true; /* _db_collation */ } if (oid_check_cached_class_oid (OID_CACHE_HA_APPLY_INFO_CLASS_ID, class_oid)) { return true; /* HA apply-info catalog */ } return false; /* normal MVCC class */}| 분기 | 클래스 | MVCC가 disabled인 이유 |
|---|---|---|
OID_ISNULL || OID_IS_ROOTOID | Root class (스키마 metaclass) | 카탈로그 부트스트랩. 자신이 버전될 수 없음 |
oid_is_serial | _db_serial | 생성된 값은 전역적으로 즉시 보여야 함. 버전된 serial은 두 tx가 같은 값을 뽑게 함 |
OID_CACHE_COLLATION_CLASS_ID | _db_collation | 사실상 정적 메타데이터. in-place가 더 싸다 |
OID_CACHE_HA_APPLY_INFO_CLASS_ID | HA apply-info | replication 진행은 스냅샷 lag 없이 관찰되어야 함 |
레코드에 대해 “MVCC disabled”의 의미는 — 그것의 헤더는 OR_MVCC_FLAG_VALID_INSID
플래그를 들고 있지 않으므로 mvcc_ins_id가 MVCCID_ALL_VISIBLE로 읽히고,
모든 가시성/vacuum 진입점이 그 값에서 게이트된다. mvcc_satisfies_snapshot
(Ch.6)에서 첫 분기가 SNAPSHOT_SATISFIED로 short-circuit (항상 보임).
아래의 perfmon 블록은 같은 != MVCCID_ALL_VISIBLE 테스트로 자신의 ..._LOST
회계를 건너뛴다. vacuum 술어 mvcc_satisfies_vacuum은
MVCC_IS_HEADER_INSID_NOT_ALL_VISIBLE 매크로를 통해 동일한 질문을 한다.
// mvcc_satisfies_snapshot guard -- src/transaction/mvcc.cif (rec_header->mvcc_ins_id != MVCCID_ALL_VISIBLE && vacuum_is_mvccid_vacuumed (rec_header->mvcc_ins_id)) { perfmon_mvcc_snapshot (thread_p, PERF_SNAPSHOT_SATISFIES_SNAPSHOT, PERF_SNAPSHOT_RECORD_INSERTED_COMMITED_LOST, PERF_SNAPSHOT_VISIBLE); }// ... condensed: same != MVCCID_ALL_VISIBLE guard recurs in mvcc_satisfies_delete / _dirty ...불변식: disabled-class 레코드는 active/visible 기계에 절대 넘겨지지 않는다.
mvcc_ins_id == MVCCID_ALL_VISIBLE로 그 가드들은 false를 읽으므로, 행은
insert 쪽에서 committed-visible이고 un-vacuumable이다. 호출자는 행마다 다시
walk하는 대신 class당 판정을 메모이즈한다 (heap insert).
Cross-check. 함수 헤더 주석은 “root class와
_db_serial,db_partition”을 나열하지만, 코드는 collation과 HA apply-info를 체크하지 partition을 체크하지 않는다. 주석은 stale이다. 위의 필드별 표는 실제oid_check_cached_class_oid분기를 따른다.
10.7 재시작 시드 — 비트 영역 재-anchoring
섹션 제목: “10.7 재시작 시드 — 비트 영역 재-anchoring”복구 후, 메모리 내 mvcctable은 log_Gl.hdr.mvcc_next_id로 복원된 MVCCID
카운터에 다시 anchoring된다.
// mvcctable::reset_start_mvccid -- src/transaction/mvcc_table.cppvoidmvcctable::reset_start_mvccid (){ m_current_trans_status.m_active_mvccs.reset_start_mvccid (log_Gl.hdr.mvcc_next_id);
assert (m_trans_status_history_position < HISTORY_MAX_SIZE); m_trans_status_history[m_trans_status_history_position].m_active_mvccs.reset_start_mvccid (log_Gl.hdr.mvcc_next_id);
m_current_status_lowest_active_mvccid.store (log_Gl.hdr.mvcc_next_id);}세 장소, 모두 같은 복원된 카운터로 시드 — (1) 현재 status의 active-set
시작 (m_bit_area_start_mvccid, per-class 절반 아래). (2) 현재 ring 슬롯의
active-set 시작 — 다른 2047 슬롯은 손대지 않은 채, 완료가 ring을 cycle하면서
lazy하게 덮어쓰임 (Ch.8). (3) 캐시된 m_current_status_lowest_active_mvccid
스칼라가 mvcc_next_id로 설정 (아직 활성 트랜잭션 없음).
// mvcc_active_tran::reset_start_mvccid -- src/transaction/mvcc_active_tran.cppvoidmvcc_active_tran::reset_start_mvccid (MVCCID mvccid){ m_bit_area_start_mvccid = mvccid; if (m_initialized) { check_valid (); /* debug: bits past length must be zero */ }}불변식: 재시작 후, active-set origin은 다음 발급될 MVCCID와 같으며, active
영역은 비어 있다. 복구된 데이터베이스가 발급하는 모든 id는
>= mvcc_next_id이므로 비트 영역은 비어 있고 정확하게 위치한다.
log_manager.c의 부팅 경로와 log_recovery.c의 세 지점 (analysis 후, redo
후, 최종 패스 후)에서 호출된다. 명시적으로 // not thread safe이며, 어떤
worker도 스냅샷을 빌드하기 전 single-threaded로 실행된다.
10.8 미해결 질문
섹션 제목: “10.8 미해결 질문”-
느린 스냅샷 빌드 하에서의 2048-ring saturation. Chapter 5의
build_mvcc_info는 슬롯의m_active_mvccs를 복사한 뒤 캡처된trans_status_version을m_version.load ()에 다시 확인하고, mismatch 시 reset하고 루프한다. 재시도는 단일 동시 mutation에 대해 방어하지만, 단일 중단 없는copy_to동안 풀-링 (HISTORY_MAX_SIZE = 2048) 덮어쓰기 — 슬롯이 구분 가능한 버전 변경 없이 mutate되는 경우 — 에 대해 입증 가능하게 안전한 지는 코드나 주석으로 확립되어 있지 않다. 실제로는 예상되지 않지만, 그것을 강제하는 명시적 경계는 없다. -
complete_sub_mvcc의 정보 필드 버그 (§10.5).m_last_completed_mvccid에 대한 이중 할당 (두 번째가SUBTRANenum을 씀)과 절대 할당되지 않는m_event_type은complete_mvcc에서의 copy-paste 실수처럼 보인다. 무해하다 (둘 다// just for info), 하지만 sub-tran 슬롯의 필드를 신뢰하는 reader는 MVCCID가 아니라 enum을 얻는다. -
is_sub_activewrite 경로.mvcc_info::copy_to에서 복사되지만 여기서 읽는 MVCC core 코드에 의해 절대 설정되지 않는다. 그 producer는 savepoint/system operation 계층에 산다. 가시성에는sub_ids의 비어 있음이 동작 신호다.
sub-transaction 경계 vs lock 획득, escalation, SERIALIZABLE write-skew 감지 —
MVCC 가시성만으로는 부족하고 lock이 간극을 메워야 하는 곳 — 에 대해서는
lock-manager detail 동반 문서 (cubrid-lock-manager-detail.md)의 escalation과
serializable conflict handling 챕터를 참고하라.
10.9 챕터 요약 — 핵심 정리
섹션 제목: “10.9 챕터 요약 — 핵심 정리”- sub-transaction은 LIFO
sub_ids스택에 자기 MVCCID를 받는다.logtb_get_new_subtransaction_mvccid는 한 id (부모 stamped) 또는get_two_new_mvccid를 통해 원자적으로 두 개 (부모 unstamped)를 할당하며, 항상 부모를 모든 sub-id 아래로 유지한다. - 부모는 자신과 자신의 sub들의 쓰기를 본다 —
logtb_is_current_mvccid를 통해,id를 체크한 뒤 전체sub_ids벡터를 선형 스캔한다 — 꼭대기만이 아니다 — 그래서 더 이른 여전히 열려 있는 sub의 쓰기도 “내 것”으로 다시 읽힌다. - sub-completion은 스냅샷 fix-up이지 vacuum 이벤트가 아니다.
logtb_complete_sub_mvcc는 스냅샷의highest_completed_mvccid를 sub-id 너머로 올리고 active-set에서 클리어한다.mvcctable::complete_sub_mvcc는SUBTRAN슬롯을 publish하지만advance_oldest_active를 건너뛴다 — 열린 부모의 sub는 절대 oldest일 수 없기 때문이다. - MVCC-disabled 클래스는 insert id를 들고 있지 않다.
mvcc_is_mvcc_disabled_class는 root class,_db_serial,_db_collation, 그리고 HA apply-info에 대해 true를 반환한다. 그것들의 레코드는MVCCID_ALL_VISIBLE로 읽혀, 모든 가시성/vacuum 가드를 “항상 보임, 절대 회수되지 않음”으로 short-circuit한다. - 재시작은 log 헤더에서 테이블을 재-anchoring한다.
reset_start_mvccid는 현재 status와 현재 ring 슬롯의 bit-area origin과 캐시된 lowest-active 스칼라를log_Gl.hdr.mvcc_next_id로 설정하고, active 영역을 비워둔다 — 부팅 동안과 세 복구 체크포인트에서 single-threaded로. - 잠재된 두 이슈가 미해결 질문이다 — 단일 진행 중 스냅샷 복사 동안의
풀-링 (2048) 덮어쓰기, 그리고
complete_sub_mvcc의 정보 필드 이중 할당 — 둘 다 관찰된 경로에서는 정확성에 영향을 주지 않는다.
이 리비전 시점의 위치 힌트
섹션 제목: “이 리비전 시점의 위치 힌트”| Symbol | File | Line |
|---|---|---|
OR_MVCC_INSERT_ID_OFFSET | src/base/object_representation.h | 483 |
OR_MVCC_DELETE_ID_OFFSET | src/base/object_representation.h | 486 |
OR_MVCC_PREV_VERSION_LSA_OFFSET | src/base/object_representation.h | 490 |
OR_GET_MVCC_FLAG | src/base/object_representation.h | 548 |
OR_MVCC_MAX_HEADER_SIZE | src/base/object_representation_constants.h | 142 |
OR_MVCC_MIN_HEADER_SIZE | src/base/object_representation_constants.h | 145 |
OR_MVCC_FLAG_MASK | src/base/object_representation_constants.h | 160 |
OR_MVCC_FLAG_VALID_INSID | src/base/object_representation_constants.h | 165 |
OR_MVCC_FLAG_VALID_DELID | src/base/object_representation_constants.h | 168 |
OR_MVCC_FLAG_VALID_PREV_VERSION | src/base/object_representation_constants.h | 171 |
or_mvcc_get_header | src/base/object_representation_sr.c | 4237 |
or_mvcc_set_header | src/base/object_representation_sr.c | 4296 |
or_mvcc_add_header | src/base/object_representation_sr.c | 4381 |
or_mvcc_get_flag | src/base/object_representation_sr.c | 4473 |
or_mvcc_set_flag | src/base/object_representation_sr.c | 4488 |
or_mvcc_get_insid | src/base/object_representation_sr.c | 4517 |
or_mvcc_set_insid | src/base/object_representation_sr.c | 4544 |
or_mvcc_get_delid | src/base/object_representation_sr.c | 4564 |
or_mvcc_get_chn | src/base/object_representation_sr.c | 4592 |
or_mvcc_set_delid | src/base/object_representation_sr.c | 4617 |
or_mvcc_set_chn | src/base/object_representation_sr.c | 4638 |
or_mvcc_set_prev_version_lsa | src/base/object_representation_sr.c | 4654 |
or_mvcc_get_prev_version_lsa | src/base/object_representation_sr.c | 4680 |
PERF_SNAPSHOT_SATISFIES_SNAPSHOT | src/base/perf_monitor.h | 238 |
PERF_SNAPSHOT_RECORD_INSERTED_VACUUMED | src/base/perf_monitor.h | 246 |
PERF_SNAPSHOT_RECORD_INSERTED_COMMITED_LOST | src/base/perf_monitor.h | 250 |
PERF_SNAPSHOT_RECORD_INSERTED_DELETED | src/base/perf_monitor.h | 252 |
PERF_SNAPSHOT_RECORD_DELETED_COMMITTED_LOST | src/base/perf_monitor.h | 257 |
perfmon_mvcc_snapshot | src/base/perf_monitor.h | 1693 |
mvcc_header_size_lookup | src/object/object_representation.c | 70 |
vacuum_data_entry | src/query/vacuum.c | 104 |
vacuum_boot | src/query/vacuum.c | 1291 |
vacuum_heap_page | src/query/vacuum.c | 1577 |
vacuum_master_task::execute | src/query/vacuum.c | 3002 |
vacuum_master_task::is_cursor_entry_ready_to_vacuum | src/query/vacuum.c | 3106 |
vacuum_process_log_block | src/query/vacuum.c | 3251 |
is_not_vacuumed_and_lost | src/query/vacuum.c | 7379 |
vacuum_rv_check_at_undo | src/query/vacuum.c | 7627 |
vacuum_is_mvccid_vacuumed | src/query/vacuum.h | 271 |
heap_get_mvcc_header | src/storage/heap_file.c | 7747 |
heap_attrinfo_transform_header_to_disk | src/storage/heap_file.c | 11937 |
heap_mvcc_log_insert | src/storage/heap_file.c | 16371 |
heap_rv_mvcc_redo_insert | src/storage/heap_file.c | 16442 |
heap_get_mvcc_rec_header_from_overflow | src/storage/heap_file.c | 19541 |
heap_insert_adjust_recdes_header | src/storage/heap_file.c | 20540 |
NULL_CHN | src/storage/storage_common.h | 66 |
MVCCID | src/storage/storage_common.h | 186 |
MVCCID_NULL | src/storage/storage_common.h | 327 |
MVCCID_ALL_VISIBLE | src/storage/storage_common.h | 329 |
MVCCID_FIRST | src/storage/storage_common.h | 330 |
MVCCID_IS_NORMAL | src/storage/storage_common.h | 335 |
MVCCID_FORWARD | src/storage/storage_common.h | 343 |
xlocator_upgrade_instances_domain | src/transaction/locator_sr.c | 12126 |
log_Gl.mvcc_table | src/transaction/log_impl.h | 707 |
mvcc_next_id | src/transaction/log_storage.hpp | 131 |
logtb_expand_trantable | src/transaction/log_tran_table.c | 251 |
logtb_define_trantable | src/transaction/log_tran_table.c | 366 |
logtb_get_number_of_total_tran_indices | src/transaction/log_tran_table.c | 696 |
logtb_rv_assign_mvccid_for_undo_recovery | src/transaction/log_tran_table.c | 1115 |
logtb_invalidate_snapshot_data | src/transaction/log_tran_table.c | 3861 |
logtb_find_current_mvccid | src/transaction/log_tran_table.c | 3910 |
logtb_get_current_mvccid | src/transaction/log_tran_table.c | 3939 |
logtb_is_current_mvccid | src/transaction/log_tran_table.c | 3972 |
logtb_get_mvcc_snapshot | src/transaction/log_tran_table.c | 4007 |
logtb_complete_mvcc | src/transaction/log_tran_table.c | 4050 |
logtb_get_new_subtransaction_mvccid | src/transaction/log_tran_table.c | 4547 |
logtb_assign_subtransaction_mvccid | src/transaction/log_tran_table.c | 4578 |
logtb_complete_sub_mvcc | src/transaction/log_tran_table.c | 4593 |
log_tdes::lock_global_oldest_visible_mvccid | src/transaction/log_tran_table.c | 6220 |
log_tdes::unlock_global_oldest_visible_mvccid | src/transaction/log_tran_table.c | 6230 |
MVCC_IS_REC_INSERTER_ACTIVE | src/transaction/mvcc.c | 46 |
MVCC_IS_REC_DELETER_ACTIVE | src/transaction/mvcc.c | 49 |
MVCC_IS_REC_INSERTER_IN_SNAPSHOT | src/transaction/mvcc.c | 52 |
MVCC_IS_REC_DELETER_IN_SNAPSHOT | src/transaction/mvcc.c | 55 |
MVCC_IS_REC_INSERTED_SINCE_MVCCID | src/transaction/mvcc.c | 58 |
MVCC_IS_REC_DELETED_SINCE_MVCCID | src/transaction/mvcc.c | 61 |
mvcc_is_id_in_snapshot | src/transaction/mvcc.c | 90 |
mvcc_is_active_id | src/transaction/mvcc.c | 122 |
mvcc_satisfies_snapshot | src/transaction/mvcc.c | 156 |
mvcc_is_not_deleted_for_snapshot | src/transaction/mvcc.c | 280 |
mvcc_satisfies_vacuum | src/transaction/mvcc.c | 321 |
mvcc_satisfies_delete | src/transaction/mvcc.c | 389 |
mvcc_satisfies_dirty | src/transaction/mvcc.c | 513 |
mvcc_is_mvcc_disabled_class | src/transaction/mvcc.c | 628 |
mvcc_snapshot::copy_to | src/transaction/mvcc.c | 679 |
mvcc_info::copy_to | src/transaction/mvcc.c | 714 |
mvcc_rec_header | src/transaction/mvcc.h | 38 |
MVCC_REC_HEADER_INITIALIZER | src/transaction/mvcc.h | 47 |
MVCC_IS_HEADER_DELID_VALID | src/transaction/mvcc.h | 87 |
MVCC_IS_HEADER_INSID_NOT_ALL_VISIBLE | src/transaction/mvcc.h | 91 |
MVCC_IS_HEADER_ALL_VISIBLE | src/transaction/mvcc.h | 95 |
MVCC_IS_REC_INSERTED_BY_ME | src/transaction/mvcc.h | 118 |
MVCC_IS_REC_DELETED_BY_ME | src/transaction/mvcc.h | 122 |
MVCC_IS_REC_DELETED_BY | src/transaction/mvcc.h | 130 |
MVCC_ID_PRECEDES | src/transaction/mvcc.h | 141 |
MVCC_ID_FOLLOW_OR_EQUAL | src/transaction/mvcc.h | 142 |
MVCC_GET_PREV_VERSION_LSA | src/transaction/mvcc.h | 156 |
mvcc_satisfies_snapshot_result | src/transaction/mvcc.h | 159 |
MVCC_SNAPSHOT_FUNC | src/transaction/mvcc.h | 171 |
mvcc_snapshot | src/transaction/mvcc.h | 173 |
mvcc_info | src/transaction/mvcc.h | 196 |
mvcc_satisfies_delete_result | src/transaction/mvcc.h | 222 |
mvcc_satisfies_vacuum_result | src/transaction/mvcc.h | 232 |
mvcc_active_tran::mvcc_active_tran | src/transaction/mvcc_active_tran.cpp | 31 |
mvcc_active_tran::initialize | src/transaction/mvcc_active_tran.cpp | 47 |
mvcc_active_tran::finalize | src/transaction/mvcc_active_tran.cpp | 62 |
mvcc_active_tran::reset | src/transaction/mvcc_active_tran.cpp | 74 |
mvcc_active_tran::long_tran_max_size | src/transaction/mvcc_active_tran.cpp | 99 |
mvcc_active_tran::bit_size_to_unit_size | src/transaction/mvcc_active_tran.cpp | 105 |
mvcc_active_tran::units_to_bits | src/transaction/mvcc_active_tran.cpp | 111 |
mvcc_active_tran::units_to_bytes | src/transaction/mvcc_active_tran.cpp | 117 |
mvcc_active_tran::get_mask_of | src/transaction/mvcc_active_tran.cpp | 123 |
mvcc_active_tran::get_bit_offset | src/transaction/mvcc_active_tran.cpp | 129 |
mvcc_active_tran::get_mvccid | src/transaction/mvcc_active_tran.cpp | 135 |
mvcc_active_tran::get_unit_of | src/transaction/mvcc_active_tran.cpp | 141 |
mvcc_active_tran::is_set | src/transaction/mvcc_active_tran.cpp | 147 |
mvcc_active_tran::get_area_size | src/transaction/mvcc_active_tran.cpp | 153 |
mvcc_active_tran::get_bit_area_memsize | src/transaction/mvcc_active_tran.cpp | 159 |
mvcc_active_tran::compute_highest_completed_mvccid | src/transaction/mvcc_active_tran.cpp | 171 |
mvcc_active_tran::compute_lowest_active_mvccid | src/transaction/mvcc_active_tran.cpp | 220 |
mvcc_active_tran::copy_to | src/transaction/mvcc_active_tran.cpp | 280 |
mvcc_active_tran::is_active | src/transaction/mvcc_active_tran.cpp | 318 |
mvcc_active_tran::remove_long_transaction | src/transaction/mvcc_active_tran.cpp | 356 |
mvcc_active_tran::add_long_transaction | src/transaction/mvcc_active_tran.cpp | 377 |
mvcc_active_tran::ltrim_area | src/transaction/mvcc_active_tran.cpp | 386 |
mvcc_active_tran::set_bitarea_mvccid | src/transaction/mvcc_active_tran.cpp | 414 |
mvcc_active_tran::cleanup_migrate_to_long_transations | src/transaction/mvcc_active_tran.cpp | 462 |
mvcc_active_tran::set_inactive_mvccid | src/transaction/mvcc_active_tran.cpp | 493 |
mvcc_active_tran::reset_start_mvccid | src/transaction/mvcc_active_tran.cpp | 506 |
mvcc_active_tran::reset_active_transactions | src/transaction/mvcc_active_tran.cpp | 517 |
mvcc_active_tran::check_valid | src/transaction/mvcc_active_tran.cpp | 525 |
mvcc_active_tran | src/transaction/mvcc_active_tran.hpp | 31 |
mvcc_active_tran::unit_type | src/transaction/mvcc_active_tran.hpp | 63 |
BITAREA_MAX_SIZE | src/transaction/mvcc_active_tran.hpp | 65 |
mvcc_active_tran::BITAREA_MAX_SIZE | src/transaction/mvcc_active_tran.hpp | 65 |
UNIT_BIT_COUNT | src/transaction/mvcc_active_tran.hpp | 69 |
BITAREA_MAX_MEMSIZE | src/transaction/mvcc_active_tran.hpp | 71 |
BITAREA_MAX_BITS | src/transaction/mvcc_active_tran.hpp | 72 |
ALL_ACTIVE | src/transaction/mvcc_active_tran.hpp | 74 |
mvcc_active_tran::ALL_ACTIVE | src/transaction/mvcc_active_tran.hpp | 74 |
mvcc_active_tran::ALL_COMMITTED | src/transaction/mvcc_active_tran.hpp | 75 |
mvcc_active_tran::m_bit_area | src/transaction/mvcc_active_tran.hpp | 78 |
mvcc_active_tran::m_bit_area_start_mvccid | src/transaction/mvcc_active_tran.hpp | 80 |
mvcc_active_tran::m_bit_area_length | src/transaction/mvcc_active_tran.hpp | 82 |
mvcc_active_tran::m_long_tran_mvccids | src/transaction/mvcc_active_tran.hpp | 85 |
mvcc_active_tran::m_long_tran_mvccids_length | src/transaction/mvcc_active_tran.hpp | 87 |
mvcc_active_tran::m_initialized | src/transaction/mvcc_active_tran.hpp | 89 |
oldest_active_set | src/transaction/mvcc_table.cpp | 92 |
oldest_active_get | src/transaction/mvcc_table.cpp | 102 |
mvcc_trans_status::mvcc_trans_status | src/transaction/mvcc_table.cpp | 116 |
mvcc_trans_status::initialize | src/transaction/mvcc_table.cpp | 128 |
mvcc_trans_status::finalize | src/transaction/mvcc_table.cpp | 135 |
mvcctable::advance_oldest_active | src/transaction/mvcc_table.cpp | 142 |
mvcctable::mvcctable | src/transaction/mvcc_table.cpp | 164 |
mvcctable::initialize | src/transaction/mvcc_table.cpp | 184 |
mvcctable::alloc_transaction_lowest_active | src/transaction/mvcc_table.cpp | 199 |
mvcctable::finalize | src/transaction/mvcc_table.cpp | 212 |
mvcctable::build_mvcc_info | src/transaction/mvcc_table.cpp | 226 |
mvcctable::compute_oldest_visible_mvccid | src/transaction/mvcc_table.cpp | 355 |
mvcctable::is_active | src/transaction/mvcc_table.cpp | 422 |
mvcctable::next_trans_status_start | src/transaction/mvcc_table.cpp | 441 |
mvcctable::next_tran_status_finish | src/transaction/mvcc_table.cpp | 455 |
mvcctable::complete_mvcc | src/transaction/mvcc_table.cpp | 465 |
mvcctable::complete_sub_mvcc | src/transaction/mvcc_table.cpp | 541 |
mvcctable::get_new_mvccid | src/transaction/mvcc_table.cpp | 565 |
mvcctable::get_two_new_mvccid | src/transaction/mvcc_table.cpp | 579 |
mvcctable::reset_transaction_lowest_active | src/transaction/mvcc_table.cpp | 593 |
mvcctable::reset_start_mvccid | src/transaction/mvcc_table.cpp | 599 |
mvcctable::get_global_oldest_visible | src/transaction/mvcc_table.cpp | 611 |
mvcctable::update_global_oldest_visible | src/transaction/mvcc_table.cpp | 617 |
mvcctable::lock_global_oldest_visible | src/transaction/mvcc_table.cpp | 632 |
mvcctable::unlock_global_oldest_visible | src/transaction/mvcc_table.cpp | 638 |
mvcctable::is_global_oldest_visible_locked | src/transaction/mvcc_table.cpp | 645 |
mvcc_trans_status | src/transaction/mvcc_table.hpp | 40 |
mvcctable | src/transaction/mvcc_table.hpp | 64 |
HISTORY_MAX_SIZE | src/transaction/mvcc_table.hpp | 97 |
mvcctable::HISTORY_MAX_SIZE | src/transaction/mvcc_table.hpp | 97 |
HISTORY_INDEX_MASK | src/transaction/mvcc_table.hpp | 98 |
mvcctable::m_transaction_lowest_visible_mvccids | src/transaction/mvcc_table.hpp | 101 |
mvcctable::m_current_status_lowest_active_mvccid | src/transaction/mvcc_table.hpp | 104 |
mvcctable::m_current_trans_status | src/transaction/mvcc_table.hpp | 107 |
m_trans_status_history_position | src/transaction/mvcc_table.hpp | 110 |
mvcctable::m_trans_status_history_position | src/transaction/mvcc_table.hpp | 110 |
mvcctable::m_trans_status_history | src/transaction/mvcc_table.hpp | 111 |
mvcctable::m_oldest_visible | src/transaction/mvcc_table.hpp | 118 |
mvcctable::m_ov_lock_count | src/transaction/mvcc_table.hpp | 119 |
Sources
섹션 제목: “Sources”cubrid-mvcc.md— 상위 동반 문서 (설계 의도, 이론).raw/code-analysis/cubrid/storage/mvcc/아래의 원시 분석.- 코드 —
src/transaction/mvcc.{h,c},mvcc_table.{hpp,cpp},mvcc_active_tran.{hpp,cpp}. MVCC 레코드 헤더는src/storage/heap_file.c에. vacuum 협조는src/transaction/vacuum.c에. - 방법론 —
knowledge/methodology/code-analysis-detail-doc.md.