콘텐츠로 이동

CUBRID MVCC — 코드 수준 심층 분석

이 문서의 위치: 상위 분석서 cubrid-mvcc.md가 설계 의도와 이론적 배경을 다룬다면, 이 문서는 코드 수준에서 모든 분기와 필드를 추적하는 심층 분석서다. 각 챕터는 독립적으로 읽을 수 있지만, 순서대로 읽으면 한 행 버전(row version)과 그 가시성을 결정하는 스냅샷이 커널 안에서 거치는 전체 생애주기를 따라갈 수 있다.

목차:

Ch제목상태
1자료구조 전체 지도
2초기화와 메모리 배치
3MVCCID 탄생과 레코드 내 헤더
4Active-Set 조회 — 비트 영역 탐색과 캐시된 스칼라
5스냅샷 생성
6가시성 판정
7Delete, Dirty, Vacuum용 형제 술어들
8커밋과 History Ring 전진
9Vacuum 협조와 Oldest-Visible 워터마크
10Sub-Transaction과 특수 경로

MVCC 모듈이 소유하는 모든 구조체를 필드 단위로 살펴본다. Snapshot Isolation 이론은 여기서 다시 유도하지 않는다 — cubrid-mvcc.md를 참고하라.

Header정의된 구조체
mvcc.hmvcc_rec_header, mvcc_snapshot, mvcc_info, 세 개의 result enum
mvcc_active_tran.hppmvcc_active_tran (비트 영역 기반 active-set 엔진)
mvcc_table.hppmvcc_trans_status, mvcctable (전역 코디네이터)
storage_common.hMVCCID typedef와 sentinel 값 사다리

1.1 MVCCID 타입과 sentinel 값 사다리

섹션 제목: “1.1 MVCCID 타입과 sentinel 값 사다리”

부호 없는 64비트 카운터 하나가 전부다. 낮은 값들은 예약된 sentinel이며 (id 1, 2는 건너뛰고, 실제로 발급되는 첫 id는 4다).

// MVCCID + sentinels -- src/storage/storage_common.h
typedef UINT64 MVCCID; /* MVCC ID */
#define MVCCID_NULL (0)
#define MVCCID_ALL_VISIBLE ((MVCCID) 3) /* visible for all transactions */
#define MVCCID_FIRST ((MVCCID) 4)
이름역할 / 이유
0MVCCID_NULL”id 없음” — 미설정/초기화 안 된 필드
3MVCCID_ALL_VISIBLE어떤 활성 스냅샷보다도 오래된 상태. 모든 트랜잭션에게 보임 (VACUUM이 insert id를 벗겨낼 때 이 값을 채운다)
4MVCCID_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.h
struct 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를 같은 워드에 함께 패킹
chnchange 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_VERSIONOR_MVCC_FLAG_MASK 아래에 묶여 있다.

불변식 — DELIDchn은 물리적 union이 아니라 논리적으로 배타적이다. 디스크 헤더는 둘 중 하나만 기록한다 (OR_MVCC_FLAG_VALID_DELID가 선택을 결정한다). MVCC_IS_HEADER_DELID_VALID는 이 플래그와 MVCCID_IS_VALID를 모두 확인하므로, chn이 트랜잭션 id로 잘못 해석되는 일은 일어나지 않는다.

초기화 매크로는 플래그와 repid를 0으로 두고 chnNULL_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_snapshotmvcc_trans_status 양쪽에 값으로 포함되어 있다. Chapter 4에서 탐색 방식을 자세히 다룬다.

// mvcc_active_tran private state -- src/transaction/mvcc_active_tran.hpp
using 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_area64비트 워드 배열. 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_lengthoverflow 스캔 범위
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.h
struct 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 읽기를 모두 처리
validfalse면 재사용된 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_infotdes마다 붙는 트랜잭션 MVCC 상태

섹션 제목: “1.5 mvcc_info — tdes마다 붙는 트랜잭션 MVCC 상태”

활성 트랜잭션 디스크립터(log_tdes)당 하나씩 존재한다.

// mvcc_info -- src/transaction/mvcc.h
struct 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_idssub-transaction id 배열. 중첩될 때마다 하나씩 (Ch 10)
is_sub_activesub-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.hpp
struct 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_versionseqlock의 핵심. 읽고-복사하고-다시 읽었을 때 값이 같으면 일관된 복사 (Ch 5)

불변식 — m_version이 일관된 복사의 시작과 끝을 표시한다. committer는 비트를 변경하기 전후로 이 값을 올린다. reader가 복사 전후로 같은 버전을 보았다면, 중간에 다른 쓰기가 끼어들지 않았음을 알 수 있다 (Ch 5). 진단용 두 필드는 이 계약과 관련이 없다.

log_Gl.mvcc_table에 인스턴스가 하나 존재한다. id 할당, 상태 히스토리 ring, 트랜잭션별 lowest-visible 배열, VACUUM을 구동하는 oldest-visible 워터마크를 모두 소유한다 (lowest_active_mvccid_typestd::atomic<MVCCID>이다).

// mvcctable private members -- src/transaction/mvcc_table.hpp
static const size_t HISTORY_MAX_SIZE = 2048; // must be a power of 2
static 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_historyHISTORY_MAX_SIZE 길이의 ring. reader가 안정적인 과거 버전을 잡아간다 (Ch 5/8)
m_new_mvccid_lockget_new_mvccid / get_two_new_mvccid를 직렬화 — 단조 id 보장
m_active_trans_mutex현재 status 변경과 history 전진을 직렬화
m_oldest_visible전역 워터마크. VACUUM은 이보다 새 버전을 회수하지 못한다
m_ov_lock_countsoft 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).

가시성 판정은 bool이 아니라 타입화된 enum을 돌려준다 — 그래야 호출자가 “너무 오래된” 경우와 “너무 새로운” 경우를 구분할 수 있다 (후자는 버전 체인을 따라 거슬러 올라간다).

// mvcc_satisfies_snapshot_result -- src/transaction/mvcc.h
enum 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.h
enum 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.h
enum 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에서 그것들을 생성하는 함수들과 짝지어 자세히 분석한다.

log_Glmvcctable 하나, 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를 따라간다.

  • 레거시 매크로 MVCC_IS_REC_DELETED_BYmvcc_rec_header에서 사라진 delid_chn.mvcc_del_id union 멤버를 여전히 dereference한다 (현재 헤더는 chnmvcc_del_id가 별도 필드다). 죽은 코드이며, 실제 읽기는 MVCC_GET_DELID / MVCC_IS_HEADER_DELID_VALID를 사용한다.
  1. MVCCID는 하위에 예약 영역을 두는 64비트 카운터다 (0/3/4 = MVCCID_NULL/MVCCID_ALL_VISIBLE/MVCCID_FIRST). MVCCID_FORWARD가 실제 id를 이 영역 위로만 유지한다.
  2. mvcc_rec_header는 디스크에 기록되는 유일한 MVCC 구조체다. mvcc_flag:8mvcc_ins_id, mvcc_del_id (vs chn), prev_version_lsa 중 어느 것이 존재하는지를 결정한다. DELIDchn은 디스크에서 배타적이지만 메모리에선 별개의 필드다.
  3. mvcc_active_tran은 슬라이딩 비트 윈도 + long-tran overflow 배열이다. offset은 id - m_bit_area_start_mvccid로 계산하며, 윈도 크기는 최대 500 × 64 id다. volatile 필드가 lock-free 읽기를 가능하게 한다.
  4. mvcc_snapshot은 두 워터마크로 비트 영역을 감싸고, 교체 가능한 snapshot_fnc를 받는다. 복사는 오직 copy_to를 통해서만 이루어진다.
  5. mvcc_infotdes마다 스냅샷, 자신의 id, 빠른 recent_snapshot_lowest_active_mvccid 컷오프, sub-transaction 상태를 묶어주는 묶음이다.
  6. mvcc_trans_statusm_version만으로 검증되는 seqlock-versioned active-set 스냅샷이다. 나머지 두 필드는 진단용이다.
  7. mvcctable은 단일 전역 코디네이터다 — id 발급, 2의 거듭제곱 history ring, lowest-visible 배열, 그리고 VACUUM을 게이트하는 단조-증가 pinnable m_oldest_visible 워터마크를 소유한다.

이 챕터의 독자 질문은 이렇다 — 어떤 트랜잭션이 시작되기 전에 각 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의 핵심이다. 이 챕터에서 다루는 구조체들은 오로지 파생된 상태만을 보관한다.

  • 동적, tran-index당logtb_get_number_of_total_tran_indices () (= log_Gl.trantable.num_total_indices) 길이: m_long_tran_mvccidsm_transaction_lowest_visible_mvccids. tran 테이블이 커지면 크기가 다시 잡힌다.
  • 정적, 컴파일 시점 고정 — 비트 영역 BITAREA_MAX_SIZE = 500 워드, ring HISTORY_MAX_SIZE = 2048 슬롯. 절대 리사이즈하지 않으며, overflow는 이전 (비트 영역에서 long-tran 배열로)이거나 덮어쓰기 (ring이 한 바퀴 도는 것)다. 재할당은 절대 없다.
// mvcc_active_tran (private) -- src/transaction/mvcc_active_tran.hpp
using unit_type = std::uint64_t;
static const size_t BITAREA_MAX_SIZE = 500; // 500 units, fixed
static const size_t UNIT_BIT_COUNT = sizeof (unit_type) * BYTE_BIT_COUNT; // 64
static const size_t BITAREA_MAX_MEMSIZE = BITAREA_MAX_SIZE * UNIT_BYTE_COUNT; // 4000 bytes
static const size_t BITAREA_MAX_BITS = BITAREA_MAX_SIZE * UNIT_BIT_COUNT; // 32000 bits
static 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.cpp
size_t bit_size_to_unit_size (size_t b) { return (b + UNIT_BIT_COUNT - 1) / UNIT_BIT_COUNT; } // bits->words, ceil
size_t units_to_bits (size_t n) { return n * UNIT_BIT_COUNT; } // words -> bits
size_t units_to_bytes (size_t n) { return n * UNIT_BYTE_COUNT; } // words -> bytes
size_t get_area_size () const { return bit_size_to_unit_size (m_bit_area_length); } // LIVE words
size_t get_bit_area_memsize () const { return units_to_bytes (get_area_size ()); } // LIVE bytes

get_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.cpp
mvcc_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.cpp
void 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_transactionm_long_tran_mvccids_length < long_tran_max_size ()를 assert한다.

finalize는 두 배열을 해제하고, 포인터를 null로 만들고, 플래그를 떨어뜨린다 — ~mvcc_active_tran과 달리 객체를 다시 initialize할 수 있도록 상태를 reset 한다. resetreset_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.cpp
void 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.cpp
void 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.cpp
void 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 — 모든 필드”
FieldRoleWhy it exists
m_bit_areaBITAREA_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_initializedinitialize 실행됐고 아직 finalize 안 됐는지idempotent init, 안전한 no-op reset

세 필드의 volatilem_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.hpp
struct 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.cpp
mvcc_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 — 모든 필드”
FieldRoleWhy it exists
m_active_mvccs이 스냅샷의 active-set 비트맵본체. 나머지는 메타데이터
m_last_completed_mvccid여기서 마지막으로 커밋/롤백된 MVCCID정보/디버그용. 가시성은 무시
m_event_typeCOMMIT/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.cpp
void 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_infois_activeassert (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.cpp
void 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.cpp
void 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_infoassert (tdes.tran_index < logtb_get_number_of_total_tran_indices ())로 방어한다.

FieldRoleWhy it exists
m_transaction_lowest_visible_mvccidstran 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 슬롯의 ringwriter를 블록하지 않고도 안정된 과거 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.cpp
void 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.cpp
void 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_mvccidlog_initialize_internallog_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는 리사이즈마다 다시 실행된다.

  1. MVCCID 카운터는 테이블에 없다log_Gl.hdr.mvcc_next_id에 살며, m_new_mvccid_lock 하의 get_new_mvccid/get_two_new_mvccid만 전진시킨다. 테이블은 reset_start_mvccid로 재동기화되는 파생 마커만 보관한다.
  2. 두 가지 크기 축m_long_tran_mvccidsm_transaction_lowest_visible_mvccids는 동적 (트랜잭션 인덱스 수에 맞춰) 이고, 비트 영역 (500 워드 = 32000 비트)과 ring (2048)은 정적이다. overflow는 이전 (비트 영역에서 long-tran으로) 또는 wrap (ring)으로 흡수한다.
  3. ALL_ACTIVE == 0이 zero-init을 의미 있게 만든다 — 모든 new[]()가 이미 “모두 활성, 길이 0”을 뜻하므로 initialize가 비트를 따로 비울 필요가 없고, trailing-words-clear 불변식이 공짜로 성립한다.
  4. 살아있는 테이블은 비트 영역을 2049개 들고 있다 — 현재 status + 2048 ring 슬롯. 단순한 new[2048]은 생성자만 실행하므로 슬롯별 initialize가 힙 작업을 마무리한다.
  5. initialize/reset/reset_active_transactions/finalize는 서로 다르다 — 할당, 살아 있는 prefix만 0으로 + 시작 마커를 MVCCID_NULL로, 전체 버퍼를 0으로 (failed-copy 재시도용), 해제 및 비초기화.
  6. alloc_transaction_lowest_active만이 재실행 가능한 할당이다 — 크기 변경 가드가 total_tran_indices가 다를 때만 재할당한다 (슬롯은 MVCCID_NULL로 값-초기화). logtb_expand_trantable에 연결되어 있다.
  7. reset_start_mvccid가 부팅/재시작 솔기이다 — 현재 status, 현재 ring 슬롯, 전역 lowest-active를 복구된 카운터로 다시 가리키게 한다. 단일 스레드 복구 전용이다.

Chapter 3: MVCCID 탄생과 레코드 내 헤더

섹션 제목: “Chapter 3: MVCCID 탄생과 레코드 내 헤더”

이 챕터는 다음 질문에 답한다 — 버전이 MVCCID를 언제 획득하는가, 그리고 그 도장이 힙 레코드에 어떻게 직렬화되어, delete나 prev-version 메타데이터를 쓸 일이 없던 레코드는 추가 바이트를 한 푼도 쓰지 않게 되는가? 같은 분야의 상위 문서(cubrid-mvcc.mdMVCCID 할당 정책 / 레코드별 헤더)는 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.c
if (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_recoverytdes->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_mvccidget_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.cpp
m_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).

or_mvcc_get_header가 레코드를 MVCC_REC_HEADER로 역직렬화한다. 메모리 표현은 디스크 형식보다 넓다 — 모든 필드가 RAM에는 항상 존재하고, 플래그가 설정된 필드만 다시 기록된다.

// mvcc_rec_header -- src/transaction/mvcc.h
struct 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_idMVCCID_NULL. 셋이면 → 슬롯은 무효이고, 실제 deleter MVCCID는 8바이트 뒤의미상 레코드는 CHN을 갖거나 DELID를 갖거나, 둘 중 하나일 뿐 동시에 둘 다는 아니다 — §3.4의 VALID_DELID 불변식 참고
mvcc_ins_idinserting 트랜잭션의 MVCCID탄생 도장. reader의 스냅샷과 비교하여 insert 가시성을 결정
mvcc_del_iddeleting/updating 트랜잭션의 MVCCIDdelete나 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_sizenew_mvcc_size를 비교하고 다르면 HEAP_MOVE_INSIDE_RECORD로 슬롯 영역을 늘리거나 줄여서 페이로드 바이트가 덮어쓰이지 않게 한다. 플래그가 꺼진 필드를 setter가 쓴다면 (혹은 그 반대도), 이후의 모든 offset이 어긋나 본문이 garbage로 파싱된다.

3.4.1 or_mvcc_get_header — 분기 단위 추적

섹션 제목: “3.4.1 or_mvcc_get_header — 분기 단위 추적”

역직렬화는 고정된 필드 순서로 진행되며, 각 단계마다 플래그를 확인한다 — repidmvcc_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.c
    if (!(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_insidVALID_INSID가 클리어면 아무것도 쓰지 않고 NO_ERROR를 반환하며, 그렇지 않으면 or_put_bigint를 호출한다. or_mvcc_set_delidor_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.c
repid_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_headercontext->record_type으로 분기한다. REC_HOMEspage_get_record(..., PEEK)or_mvcc_get_header. PEEK 실패와 get 오류는 둘 다 assert(false)S_ERROR를 반환 (구성상 불가능 — 페이지는 latch되어 있고 슬롯은 검증된 상태). REC_BIGONE → forward 페이지가 필요하므로 overflow reader에 위임. REC_RELOCATIONcontext->forward_oid.slotidforward 페이지를 읽고 같은 get + 오류 처리. defaultassert(false), S_ERROR — 다른 유형은 호출자 버그다.

heap_get_mvcc_rec_header_from_overflow는 특수 경우다. overflow 레코드는 항상 최대 크기 헤더를 저장한다. 호출자가 peek_recdes로 NULL을 넘기면, 스택의 ovf_recdes 스크래치 버퍼로 폴백한 뒤 ->dataoverflow_get_first_page_data (ovf_page)로 가리키게 하고 ->lengthOR_MVCC_MAX_HEADER_SIZE로 강제한 다음 or_mvcc_get_header를 호출한다 — overflow 페이지에서는 선택 필드가 항상 실체화되어 있기 때문이다 (자매 함수 heap_set_mvcc_rec_header_on_overflowVALID_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.c
redo_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_insertMVCC_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_insidVALID_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). 비트 0x080x10은 마스크와 시프트는 받지만 어떤 의미도 부여되지 않으며, mvcc_header_size_lookup[8] 크기다 — 정의된 세 비트만 인덱싱하므로 0x08을 설정하면 OOB 인덱스가 된다. 헤더 주석은 의도적 예약이라 시사하지만 의도된 용도는 문서화되어 있지 않다. 네 번째 on-record MVCC 필드를 추가하려는 사람은 mvcc_header_size_lookup을 넓히고, 모든 누적 offset 매크로를 확장하고, overflow 페이지의 OR_MVCC_MAX_HEADER_SIZE = 32 단언을 감사해야 한다.

  1. MVCCID는 쓰기 트랜잭션당 한 번, 지연 발급된다. logtb_get_current_mvccidcurr_mvcc_info->idMVCCID_NULL일 때만 발급한다. 발급/유효성 테스트가 sub_ids 분기보다 무조건 먼저 실행되므로 sub-transaction 경로조차도 부모 id를 먼저 발급한다.
  2. 할당기는 작은 락-가드 카운터다. get_new_mvccidlog_Gl.hdr.mvcc_next_idm_new_mvccid_lock 하에서 읽고 MVCCID_FORWARD 한다. get_two_new_mvccid는 parent+sub 부트스트랩을 위해 두 번 전진시켜 부모에게 더 작은 id를 준다. 롤백으로 인한 gap은 무해하다.
  3. mvcc_rec_header는 디스크보다 메모리에서 더 넓다. mvcc_flag의 하위 5비트가 insid, delid, prev_version_lsa 중 어느 것이 물리적으로 존재하는지 결정하며, chn 슬롯은 delete-id 영역과 역할을 공유한다.
  4. 0바이트 슬롯은 누적 offset 매크로와 mvcc_header_size_lookup[8]에서 온다. 갱신되지 않은 살아있는 행은 16바이트 헤더를 갖는다. 범위는 OR_MVCC_MIN_HEADER_SIZE = 8OR_MVCC_MAX_HEADER_SIZE = 32다.
  5. Get/set 헬퍼는 플래그 게이트이며 포인터 일관성을 유지한다. 필드를 건너뛴 getter는 포인터를 전진시키면 안 된다. or_mvcc_set_headerHEAP_MOVE_INSIDE_RECORD로 크기를 맞추고, or_mvcc_set_flag는 하위 5비트를 비우고 새 플래그를 비트 24+ 자리에 +=로 결합한다 — 리사이즈는 안 한다, 미끄러운 부분이다.
  6. 도장은 복구/적용 경로에서 기록된다. insert는 heap_attrinfo_transform_header_to_disk / heap_insert_adjust_recdes_header를 통해 VALID_INSID 플래그가 켜지고 insid는 0인 헤더를 만든다. heap_mvcc_log_insert가 발급을 강제하고, heap_rv_mvcc_redo_insertMVCC_SET_INSID(..., rcv->mvcc_id)에서 실제 MVCCID가 자리잡는다. overflow 레코드는 항상 max-size 헤더를 저장한다.
  7. 다섯 플래그 비트 중 둘은 사용되지 않으며 [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스냅샷 가시성 항목을 참고하라. 이 챕터는 코드를 추적한다.

탐색은 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_infomvcc_snapshot을 소유하고, mvcc_snapshotmvcc_active_tran 비트 영역과 두 개의 파생 스칼라를 소유한다.

mvcc_active_tran — Chapter 1에서 매핑됨. 읽기 경로 필드만 다시 정리한다.

FieldRoleWhy it exists
m_bit_areaunit_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 — 호출자가 보는 스냅샷 레코드.

FieldRoleWhy 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에 붙어 있다.

FieldRoleWhy it exists
snapshot트랜잭션 자신의 mvcc_snapshot문장당 또는 트랜잭션당 한 번 빌드
id자신의 MVCCID (쓰지 않았으면 MVCCID_NULL)logtb_is_current_mvccid가 매칭 — 내 쓰기는 나에게 활성
recent_snapshot_lowest_active_mvccid가장 최근 스냅샷의 lowest-activemvcc_is_active_id short-circuit. 이 아래면 커밋됨, 테이블 건너뜀
sub_ids실행 중인 sub-transaction MVCCID들logtb_is_current_mvccid가 함께 매칭 (Ch. 10)
is_sub_activesub-transaction 실행 중sub-transaction 부기 (Ch. 10)

불변식 (비트 의미론). set 비트는 커밋됨, clear 비트는 활성을 뜻한다 — 자연스러운 읽기의 반대다. 따라서 is_active!is_set(position)을 반환한다. ALL_ACTIVE = 0ALL_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.cpp
size_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 불변식).

스택의 바닥이다 — “이 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보다 짧다는 사실에 의존한다.

  1. set 비트는 커밋, clear 비트는 활성을 의미한다 (자연스러운 읽기의 반대). is_active!is_set(position)을 반환하며, 극단의 워드는 ALL_ACTIVE = 0 / ALL_COMMITTED = -1이다.
  2. is_active는 순서가 정해진 세 경우다 — 윈도 아래 (정렬된 long-tran 배열 스캔, 아니면 커밋), 빈 윈도 (활성), 윈도 안/위 (비트 룩업, 또는 추적된 길이 위는 활성). 이 순서가 is_set이 빈/범위 밖 윈도에서 dereference되는 것을 막는다.
  3. 두 도출 스캔은 서로의 거울이다 — top-down으로 최상위 set 비트를 찾고, bottom-up으로 최하위 clear 비트를 찾는다. 둘 다 6단계 워드 내 이진 탐색과 명시적 fallback (start - 1, get_mvccid(m_bit_area_length))을 갖는다.
  4. compute_lowest_active_mvccidm_long_tran_mvccids[0]로 short-circuit 한다 — overflow 배열이 오름차순으로 유지되기 때문이다.
  5. 캐시된 스칼라가 탐색을 앞지른다. mvcc_is_id_in_snapshot은 두 스칼라 사이의 회색 영역에서만 비트 영역을 건드린다. mvcc_is_active_idrecent_snapshot_lowest_active_mvccid 캐시와 logtb_is_current_mvccid (자신의 id + sub-ids)를 더한다.
  6. 공유 테이블 읽기는 락 없이 낙관적이다mvcctable::is_active가 탐색 전후로 m_version을 검증하며 변경 시 재시도한다.

읽기 전용 트랜잭션은 전역 커밋 상태를 자신만의 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 레이아웃을 설명한다.

스냅샷은 하나의 객체가 아니다. mvcc_snapshotmvcc_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) — 읽기 대상이 되는 사진.

FieldRoleWhy 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 봉투.

FieldRoleWhy it exists
snapshot여기서 만든 mvcc_snapshot읽기 사진
id이 트랜잭션의 MVCCID (Ch. 3). 첫 쓰기 전까지는 MVCCID_NULL자기 가시성 확인
recent_snapshot_lowest_active_mvccid스냅샷의 lowest active 캐시 사본스냅샷 구조체 바깥에서 사용되는 두 번째 빠른 컷오프 (형제 술어, Ch. 7)
sub_idssub-transaction MVCCID 스택 (Ch. 10)savepoint / 중첩 문장 가시성
is_sub_activesub-transaction 실행 중 true특수 경로 라우팅 (Ch. 10)

mvcc_trans_status (mvcc_table.hpp) — 하나의 전역 커밋-상태 ring 슬롯.

FieldRoleWhy it exists
m_active_mvccs이 슬롯이 publish된 시점의 권위적 live 비트 영역스냅샷이 복사하는 데이터
m_last_completed_mvccid슬롯이 쓰일 때 마지막으로 완료된 MVCCID. “just for info”디버깅 / 히스토리 포렌식
m_event_typeCOMMIT/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). 여섯 필드 모두.

FieldRoleWhy it exists
m_bit_areaunit_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_mvccidsstart보다 오래된 여전히 활성인 MVCCID overflow 배열비트 영역 윈도에서 밀려난 long transaction
m_long_tran_mvccids_lengthoverflow 배열의 항목 수long-tran 복사/스캔의 범위
m_initialized버퍼가 할당되면 truecopy_to/check_valid가 버퍼 접근 전 assert

5.2 진입 가드 — 누가 스냅샷을 받을 자격이 있는가

섹션 제목: “5.2 진입 가드 — 누가 스냅샷을 받을 자격이 있는가”

모든 읽기 경로는 logtb_get_mvcc_snapshot으로 흘러든다. 이것은 builder가 아니라 가드다 — 빌드를 시도할지 말지를 결정한다.

// logtb_get_mvcc_snapshot -- src/transaction/log_tran_table.c
LOG_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;

분기 정리:

  1. 활성 worker 트랜잭션이 아님NULL 반환. 시스템 트랜잭션 (VACUUM, checkpoint, 복구)은 MVCC 사진이 없다. 호출자는 NULL을 “모든 커밋된 것을 본다”로 다룬다.
  2. 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를 부르면 안 된다.
  3. 스냅샷이 유효함 → 빌드 건너뛰기 (RR/SR의 첫 문장 이후 흔한 경우). 무효 → 빌드 후, 2단계에서 락을 잡았다면 해제. 항상 포인터를 반환.

불변식 — 유효성 epoch당 한 번의 빌드. build_mvcc_infosnapshot.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.cpp
oldest_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.cpp
index = 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.cpp
if (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_versioncopy_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.cpp
tdes.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 - 1PSTAT_LOG_SNAPSHOT_RETRY_COUNTERS에 더한다 (“minus one”이 의무적인 첫 번째 패스를 빼므로 메트릭은 경합이지 work가 아니다). 경과 시간은 PSTAT_LOG_SNAPSHOT_TIME_COUNTERS에 더한다.

복사는 크기 지정된 memcpy에 shrink-clear와 long-transaction 꼬리, 그리고 안전 플래그 게이트가 더해진 것이다.

// mvcc_active_tran::copy_to -- src/transaction/mvcc_active_tran.cpp
assert (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.cpp
std::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를 호출하지 않는다 (호출자가 재시도 중이므로 아직 일관되지 않다).

mvcc_snapshot::copy_tomvcc_info::copy_to는 hot path가 아니다 — 이미 완성된 스냅샷을 clone한다 (예 parent → sub-transaction, Ch. 10). source가 완성된 변경 없는 로컬이므로 THREAD_SAFE를 쓴다. mvcc_snapshot::copy_todest.m_active_mvccs.initialize ()를 부른 뒤 copy_to (..., THREAD_SAFE)를 호출하고, 그다음 lowest_active_mvccid, highest_completed_mvccid, snapshot_fnc, valid를 거울처럼 반영한다 — 모든 필드를. 그래서 clone은 재빌드 없이 사용 가능하다. mvcc_info::copy_tothis->snapshot.copy_to (dest.snapshot)를 호출한 뒤 봉투 필드를 덮어쓴다.

// mvcc_info::copy_to -- src/transaction/mvcc.c
dest.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.c
if (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를 건드리지 않는다.

  1. 진입 가드가 모든 것을 게이트한다. logtb_get_mvcc_snapshot은 시스템 트랜잭션에 NULL을 반환하고, parallel-px worker를 m_px_lock_mutex에서 직렬화하며, valid == false일 때만 빌드한다.
  2. 생성은 버전 재확인을 통한 lock-free다. build_mvcc_infom_versionTHREAD_UNSAFE copy_to 전후로 읽는다. 같으면 → 일관됨 (break), 다르면 → 찢어진 상태 (reset_active_transactions하고 재시도).
  3. lowest-visible dance가 VACUUM 워터마크 역행을 막는다. MVCCID_ALL_VISIBLE sentinel을 먼저 쓰고, 전역 lowest를 읽고, 중간 작업 없이 다시 쓴다. 그래야 VACUUM (Ch. 9)이 undercut하지 않고 기다린다.
  4. THREAD_UNSAFETHREAD_SAFE는 source에 관한 것이다. 빌드 경로는 live 변경 가능한 슬롯을 복사한다 (check_valid 건너뛰고 재확인에 의존). clone 경로는 정적인 로컬을 복사한다 (검증).
  5. 비트 영역 꼬리는 0으로 유지되어야 한다. copy_to의 shrink-clear, reset_active_transactions의 전체 버퍼 memset, check_valid의 assert가 m_bit_area_length 너머에 stale 커밋 비트를 남기지 않게 한다.
  6. 스칼라 필드는 고정 순서로 채워지며 valid가 마지막이다. highest_completed = MVCCID_FORWARD(...)가 배타적 상한이다. 두 lowest 필드는 전역 lowest를 받는다. snapshot_fnc = mvcc_satisfies_snapshot이다.
  7. 격리 수준 타이밍은 외부적이다. RC는 문장당 재빌드 (logtb_invalidate_snapshot_data를 통해). RR/SR은 한 번만 빌드. builder는 격리 수준에 무관하다.

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” 절에 있다. 이 챕터는 그것을 다시 유도하지 않는다.

mvcc_satisfies_snapshot은 스레드 (나는 누구인가), 레코드 헤더, 스냅샷에 대한 순수 결정 함수다 — 부수 효과가 없다. 스냅샷을 변경하는 mvcc_satisfies_dirty (Ch. 7)와는 다르다. 판정은 세 가지 enum 값 중 하나다.

// mvcc_satisfies_snapshot_result -- src/transaction/mvcc.h
enum 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_SNAPSHOTreader 입장에서 이미 죽었다. 더 오래된 버전이 살릴 수 없다멈춰라 — reader는 이 체인 head에서 아무것도 보지 않는다

방향성 불변식. TOO_NEW는 버전 체인의 뒤쪽을 가리킨다. TOO_OLD는 종착점이다. 스냅샷 안에서 커밋된 delete는 더 오래된 버전으로 되돌릴 수 없다 — 그 버전은 삭제된 같은 논리적 행이다. 따라서 TOO_NEW의 enum 주석만이 “check previous versions in log”를 언급한다. §6.7에 worked example이 있다.

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.

FieldRole hereWhy it exists
mvcc_flagMVCC_IS_HEADER_DELID_VALIDMVCC_IS_FLAG_SET(.., VALID_INSID)로 읽힘 — 어떤 도장이 있는가버전이 insert 도장을 갖지 않을 수도 (vacuum이 떼어냄) 있고 delete 도장이 없을 수도 (한 번도 삭제 안 됨) 있다
repid / chn안 읽음representation id / 캐시 일관성 번호 — 레코드 형식과 클라이언트 캐시
mvcc_ins_idinserter의 MVCCID. 스냅샷과 “나”에 비교이 버전을 나타나게 만든 트랜잭션
mvcc_del_iddeleter의 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).

FieldRole hereWhy it exists
lowest_active_mvccidmvcc_is_id_in_snapshot. 엄격히 작은 id는 committed-before (보임)의심 구간의 하한. 빠른 reject
highest_completed_mvccidmvcc_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 — 판정의 입력. 헤더는 도장과 플래그를, 스냅샷은 경계를, 스레드는 정체성을 제공한다.

첫 분기가 이 함수의 유일한 구조적 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_SNAPSHOTmvcc_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_MElogtb_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를 낳는다.

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_snapshotprev_version_lsa를 절대 건드리지 않는다. 그저 TOO_NEW_FOR_SNAPSHOT을 내보내 호출자에게 추적하라고 알린다 (링크는 insert/update 시점에 설정된다, Ch. 3). 스캔/fetch 계층 (heap_file.c)이 판정을 해석한다 — SNAPSHOT_SATISFIED는 버전 반환. TOO_OLD는 체인 head가 죽었다는 뜻. TOO_NEWprev_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 deleteins=50, del=100inserter not in-snapshot. deleter in-snapshot(g)SNAPSHOT_SATISFIED
T50 insert, 커밋된 T60 deleteins=50, del=60둘 다 not in-snapshot(h)TOO_OLD

행 2와 3이 비대칭이다 — too-new insert는 reader를 더 오래된 버전을 찾으러 뒤로 보낸다. too-new delete는 행을 보이게 유지한다 (R 입장에서 미커밋된 delete는 일어나지 않았다). insert MVCCID는 버전의 등장을 게이트하고, delete MVCCID는 사라짐을 게이트한다.

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이 아니므로 가드가 없다.

가시성 불변식. 빌드된 (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 버전)을 만든다.

  • 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 형제를 다룬다.
  1. mvcc_satisfies_snapshot은 순수하고 부수 효과가 없으며, MVCC_IS_HEADER_DELID_VALID의 상위 분기 하나가 SNAPSHOT_SATISFIED, TOO_NEW_FOR_SNAPSHOT, 또는 TOO_OLD_FOR_SNAPSHOT을 만든다.
  2. not-deleted ladder의 네 정렬된 경우 — insert 도장 없음 (보임), inserted-by-me (보임), inserter-in-snapshot (TOO_NEW), 잔여 committed-before (보임). in-snapshot inserter만 뒤로 돌아본다.
  3. deleted ladder의 네 정렬된 경우 — deleted-by-me (TOO_OLD), inserter-in-snapshot (TOO_NEW), deleter-in-snapshot (보임), 잔여 (TOO_OLD). deleter가 여전히 동시일 때만 보인다.
  4. 비대칭 — too-new insert는 이 버전을 숨긴다 (더 오래된 것을 보라). too-new delete는 보이게 유지한다. TOO_NEWprev_version_lsa를 뒤로 추적. TOO_OLD는 종착.
  5. 모든 leaf는 PERF_SNAPSHOT_* 버킷을 보고한다. _LOST 버킷은 vacuum_is_mvccid_vacuumed가 도장이 이미 사라졌어야 한다고 말할 때 발화한다 — vacuum 지연 측정치. insert side에서는 MVCCID_ALL_VISIBLE에 대한 가드가 있다.
  6. 가시성 불변식 — 보이는 것은 inserter가 보이고 deleter가 아직 보이지 않을 때. TOO_NEW는 inserter가 동시. TOO_OLD는 inserter가 보이지만 deleter가 visible-or-me. case 순서 (정체성이 경계보다 먼저)가 이를 강제한다.
  7. 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_idlive 테스트다 — 캡처된 사본이 아닌 현재 전역 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이며 테이블 탐색 없음.

네 술어는 세 개의 enum을 반환한다. mvcc_satisfies_snapshot_result (Chapter 6 에서)는 ..._dirty..._is_not_deleted_for_snapshot이 더 좁은 영역으로 재사용한다.

mvcc_satisfies_snapshot_result (..._dirty, ..._is_not_deleted_for_snapshot 반환):

FieldRoleWhy 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 반환):

FieldRoleWhy 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 반환):

FieldRoleWhy 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_mvccidsnapshot->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_SATISFIEDVALID_INSID 없음 / inserted-by-me / inserter-committed는 아무것도 안 쓴다. inserter ACTIVElowest_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_id vs oldest_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_vacuumoldest_mvccid 워터마크워터마크 SINCE아니오 — 완전히 과거인 버전만 작용3-state vacuum없음

중간 열만이 이들을 구분한다. DELID_VALID 분기, inserted-by-me short-circuit, perfmon 부기는 공통이다.

  1. 네 술어의 차이는 거의 전적으로 하나의 비교 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를 반환한다.
  2. mvcc_satisfies_deletelive 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).
  3. mvcc_satisfies_dirty는 유일한 부수 효과 술어이며 역시 live다 — SNAPSHOT_SATISFIED일 때 active inserter를 lowest_active_mvccid또는 active deleter를 highest_completed_mvccid에 (절대 둘 다는 아님) 찍는다 — 스크래치 구조체에 대한 출력이지 절대로 실제 스냅샷이 아니다.
  4. mvcc_is_not_deleted_for_snapshot은 값싼 delete 전용 동결 체크 — MVCC_IS_REC_DELETER_IN_SNAPSHOT을 통한 네 분기 (not-deleted short-circuit과 3방향 deleted arm), insert 로직 없음, TOO_NEW 없음.
  5. mvcc_satisfies_vacuum은 순수 워터마크 산술이다 — deleter가 oldest_mvccid 이전에 커밋했으면 REMOVE, inserter가 그랬으면 DELETE_INSID_PREV_VER, 그 외에는 CANNOT_VACUUM. 워터마크가 보수적으로 낮기 때문에만 안전하다.

쓰기 트랜잭션이 끝나면 세 가지가 일어나야 한다. 그것도 크래시에서 살아남고 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, 워터마크 유지만 다룬다.

커밋은 세 핵심 struct 모두를 건드린다 (mvcc_active_tran의 모든 필드 역할은 Ch. 1에 있다. 커밋과 관련된 유지 필드만 여기 반복한다).

mvcctable — 프로세스 전역 코디네이터 (인스턴스 하나, log_Gl.mvcc_table).

FieldRoleWhy it exists
m_transaction_lowest_visible_mvccidstran-index당 atomic<MVCCID>. 이 tran이 visible로 유지해야 할 가장 오래된 MVCCIDVACUUM의 floor. 커밋은 committer의 슬롯을 clamp
m_transaction_lowest_visible_mvccids_size그 배열 길이realloc 가드
m_current_status_lowest_active_mvccidatomic 전역 oldest-active 워터마크advance_oldest_active가 CAS-bump. VACUUM이 read
m_current_trans_statusmutex 하에서 변경되는 살아있는 mvcc_trans_statuslock-free로는 절대 read 안 함
m_trans_status_history_position가장 최근 publish된 status의 atomic ring index단일 가시성 store
m_trans_status_historyHISTORY_MAX_SIZE (2048) status 슬롯의 ringlock-free reader가 최근 스냅샷을 가져감
m_active_trans_mutexstatus 변경 직렬화한 번에 한 completer
m_new_mvccid_lock, m_oldest_visible, m_ov_lock_countMVCCID 발급 + oldest-visible 워터마크커밋 경로 밖. Ch. 3과 Ch. 9

mvcc_trans_status — 하나의 전역 “스냅샷 세대”.

FieldRoleWhy it exists
m_active_mvccsmvcc_active_tran 페이로드active-set 데이터
m_last_completed_mvccid이 status에 퇴역한 마지막 MVCCIDhighest_completed 힌트
m_event_typeCOMMIT, ROLLBACK, 또는 SUBTRAN세대의 사후
m_version세대마다 증가하는 atomic<version_type>reader recheck 토큰

mvcc_active_tran — 커밋 시 유지 측의 모든 필드.

FieldRole at commitWhy it exists
m_bit_area500 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_lengthlong-tran 항목 수배열 범위. compute_lowest_active_mvccid 구동
m_initialized생명주기 플래그. copy_toreset_start_mvccid가 assertinit/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.c
mvccid = 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 경로다.

본문은 명시적인 ulock.unlock ()까지 m_active_trans_mutex 아래에서 실행된다. 분기 단위 walkthrough.

  1. m_active_trans_mutex 잠금.
  2. next_trans_status_start (§8.4) — 다음 ring 슬롯 예약, 버전 증가, 슬롯 무효화.
  3. if (committed)logtb_tran_update_all_global_unique_stats. 실패 시 assert (false). else (롤백) 건너뜀.
  4. set_inactive_mvccid(mvccid) (§8.7), 그다음 m_last_completed_mvccid = mvccidm_event_type = COMMIT/ROLLBACK 설정.
  5. next_tran_status_finish (§8.5) — active-set 복사, position publish.
  6. clamp 분기if (committed) floor를 mvccid까지 위로 clamp (슬롯이 MVCCID_NULL이거나 선행할 때만). else floor를 MVCCID_NULL로 설정.
  7. 잠금 해제.
  8. 잠금 해제 후 전진if 전역 == mvccid 또는 mvccidbit_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.cpp
if (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.cpp
next_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.cpp
m_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_SAFEcopy_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.cpp
MVCCID 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_mvccidnext_status를 lock-free로 읽는다. 다른 completer가 슬롯을 재사용했다면 버전이 다를 것이고 값은 폐기된다.

// mvcctable::advance_oldest_active -- src/transaction/mvcc_table.cpp
do
{
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.cpp
if (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_transactionadd_long_transaction

섹션 제목: “8.8 remove_long_transaction과 add_long_transaction”
// mvcc_active_tran::remove_long_transaction -- src/transaction/mvcc_active_tran.cpp
assert (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.cpp
assert (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.cpp
const 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.cpp
if (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.cpp
const 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 0
if ((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_ACTIVE
for (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_mvccid
for (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되어 유지 버그를 손상 지점에서 드러낸다.

  1. read-only 커밋은 거의 공짜다. MVCCID가 없으면 logtb_complete_mvcccomplete_mvcc를 건너뛰고 가시성 슬롯만 MVCCID_NULL로 reset한다. write 트랜잭션만 mutex-보호 경로를 탄다.

  2. 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에게 원자적으로 가시화한다.

  3. 커밋은 가시성 floor를 올리고, 롤백은 떨어뜨린다. 커밋은 per-tran 슬롯을 mvccid까지 위로 clamp (절대 내리지 않음) — 그래서 VACUUM이 LOG_COMMIT이 영구 저장되기 전 데이터를 회수할 수 없다. 롤백은 즉시 MVCCID_NULL로 설정.

  4. 워터마크 전진은 lazy하고 단조다. 잠금 해제 후 complete_mvcc는 퇴역 MVCCID가 병목이었을 때만 oldest-active를 다시 계산하고, 슬롯 버전을 recheck 하고, advance_oldest_active가 CAS-bump한다. 워터마크는 오직 증가만 한다.

  5. 퇴역은 윈도 위치로 라우팅되고 자체 압축한다. 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_transationsLONG_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.hpp
using 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 */
FieldRoleWhy 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이 readvacuum 소비자당 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.cpp
cubmem::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.cpp
size_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.cpp
MVCCID 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.cpp
MVCCID 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.cpp
void 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.c
log_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.c
m_oldest_visible_mvccid = log_Gl.mvcc_table.update_global_oldest_visible ();

master는 블록 적격성을 이걸로 게이트한다.

// vacuum_master_task::is_cursor_entry_ready_to_vacuum -- src/query/vacuum.c
if (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.c
MVCCID 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; }
#endif

VACUUM_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_pageMVCCID_IS_NORMAL (threshold_mvccid)를 assert하고, 각 후보 record에 대해 Ch.7 술어의 판정으로 dispatch한다.

// vacuum_heap_page (per-record) -- src/query/vacuum.c
helper.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_VACUUMlive 스냅샷이 필요할 수 있음. 그대로 둠

술어는 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.c
struct 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된다.

  1. 워터마크는 하나의 스칼라 m_oldest_visible이다 — 어떤 live 스냅샷이든 볼 수 있는 최소 MVCCID. vacuum은 get_global_oldest_visible로 읽고, master heartbeat에서 recompute된다.
  2. 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.
  3. update_global_oldest_visiblem_ov_lock_count를 더블 체크한다 (sweep 전, store 전). 따라서 sweep 중간의 pin이 새 값을 폐기한다. store는 단조성을 assert.
  4. pin은 pin된 작업 윈도에 걸쳐 워터마크를 동결한다. lock은 locator 쪽에서 잡히고, unlock은 log_complete에서 일어나며, 그 뒤에 슬롯이 reset_transaction_lowest_activeMVCCID_NULL로 reset된다.
  5. vacuum은 워터마크를 두 번 소비한다. master 블록 gate (newest_mvccid >= m_oldest_visible_mvccid → 건너뜀)와 worker threshold_mvccidmvcc_satisfies_vacuum으로 들어간다. 그 술어 혼자가 REMOVE / DELETE_INSID_PREV_VER / CANNOT_VACUUM을 선택.
  6. 작은 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을 참고하라.

mvcc_info (트랜잭션별 상태, Ch.1/Ch.3)와 mvcc_trans_status (하나의 ring 슬롯, Ch.1/Ch.8)에 대해 sub-transaction 경로만 사용하는 필드를 다루며 마무리 한다.

// struct mvcc_info -- src/transaction/mvcc.h
struct 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 ...
};
FieldRoleWhy 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_activesub-transaction이 “현재” 쓰기 정체성을 소유하는 동안 true 설정활성 쓰기 MVCCID가 id가 아니라 sub_ids.back()임을 신호. copy_to에서 미러링되지만 MVCC core에서는 절대 읽히지 않음 — passive server를 위한 정보 상태

sub_ids는 set이 아니라 스택이다. logtb_assign_subtransaction_mvccidpush_back만 한다. logtb_complete_sub_mvccback()에서 읽은 값만 pop_back한다. out-of-order 완료는 잘못된 id를 pop한다. CUBRID의 system operation/savepoint 기계가 요구되는 LIFO 중첩을 보장한다.

// struct mvcc_trans_status -- src/transaction/mvcc_table.hpp
struct 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 ...
};
FieldRoleWhy 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_typeoldest 전진?
complete_mvcc(.., committed=true)COMMIT예 — 커밋된 작업이 floor를 올릴 수 있음
complete_mvcc(.., committed=false)ROLLBACK예 — 롤백된 ID가 active set을 떠남
complete_sub_mvccSUBTRAN 의도. 실제로는 손대지 않음 (§10.5 버그)아니오 — 부모가 여전히 열려 있어 sub-id는 절대 lowest일 수 없음

logtb_get_new_subtransaction_mvccid는 system operation이나 savepoint가 자신의 정체성으로 써야 할 때의 진입점이다.

// logtb_get_new_subtransaction_mvccid -- src/transaction/log_tran_table.c
void
logtb_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.cpp
void
mvcctable::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.c
static void
logtb_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.c
bool
logtb_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 == mvccidtrue, 부모의 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.c
STATIC_INLINE bool
mvcc_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와 비싼 전역 탐색 사이에 있다.

sub는 부모 전에 끝난다. logtb_complete_sub_mvcc가 트랜잭션별 절반을 실행한 뒤 부모의 live 스냅샷을 패치한다.

// logtb_complete_sub_mvcc -- src/transaction/log_tran_table.c
void
logtb_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의 행을 즉시 읽게 한다.

mvcctable::complete_sub_mvcc는 전역 대응물 — complete_mvcc (Ch.8)과 거의 동일하지만 oldest-active recompute를 생략한다.

// mvcctable::complete_sub_mvcc -- src/transaction/mvcc_table.cpp
void
mvcctable::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_startm_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에 주목.

mvcc_is_mvcc_disabled_class는 순전히 class OID로부터 참여를 결정한다.

// mvcc_is_mvcc_disabled_class -- src/transaction/mvcc.c
bool
mvcc_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_ROOTOIDRoot class (스키마 metaclass)카탈로그 부트스트랩. 자신이 버전될 수 없음
oid_is_serial_db_serial생성된 값은 전역적으로 즉시 보여야 함. 버전된 serial은 두 tx가 같은 값을 뽑게 함
OID_CACHE_COLLATION_CLASS_ID_db_collation사실상 정적 메타데이터. in-place가 더 싸다
OID_CACHE_HA_APPLY_INFO_CLASS_IDHA apply-inforeplication 진행은 스냅샷 lag 없이 관찰되어야 함

레코드에 대해 “MVCC disabled”의 의미는 — 그것의 헤더는 OR_MVCC_FLAG_VALID_INSID 플래그를 들고 있지 않으므로 mvcc_ins_idMVCCID_ALL_VISIBLE로 읽히고, 모든 가시성/vacuum 진입점이 그 값에서 게이트된다. mvcc_satisfies_snapshot (Ch.6)에서 첫 분기가 SNAPSHOT_SATISFIED로 short-circuit (항상 보임). 아래의 perfmon 블록은 같은 != MVCCID_ALL_VISIBLE 테스트로 자신의 ..._LOST 회계를 건너뛴다. vacuum 술어 mvcc_satisfies_vacuumMVCC_IS_HEADER_INSID_NOT_ALL_VISIBLE 매크로를 통해 동일한 질문을 한다.

// mvcc_satisfies_snapshot guard -- src/transaction/mvcc.c
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);
}
// ... 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”

복구 후, 메모리 내 mvcctablelog_Gl.hdr.mvcc_next_id로 복원된 MVCCID 카운터에 다시 anchoring된다.

// mvcctable::reset_start_mvccid -- src/transaction/mvcc_table.cpp
void
mvcctable::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.cpp
void
mvcc_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로 실행된다.

  1. 느린 스냅샷 빌드 하에서의 2048-ring saturation. Chapter 5의 build_mvcc_info는 슬롯의 m_active_mvccs를 복사한 뒤 캡처된 trans_status_versionm_version.load ()에 다시 확인하고, mismatch 시 reset하고 루프한다. 재시도는 단일 동시 mutation에 대해 방어하지만, 단일 중단 없는 copy_to 동안 풀-링 (HISTORY_MAX_SIZE = 2048) 덮어쓰기 — 슬롯이 구분 가능한 버전 변경 없이 mutate되는 경우 — 에 대해 입증 가능하게 안전한 지는 코드나 주석으로 확립되어 있지 않다. 실제로는 예상되지 않지만, 그것을 강제하는 명시적 경계는 없다.

  2. complete_sub_mvcc의 정보 필드 버그 (§10.5). m_last_completed_mvccid에 대한 이중 할당 (두 번째가 SUBTRAN enum을 씀)과 절대 할당되지 않는 m_event_typecomplete_mvcc에서의 copy-paste 실수처럼 보인다. 무해하다 (둘 다 // just for info), 하지만 sub-tran 슬롯의 필드를 신뢰하는 reader는 MVCCID가 아니라 enum을 얻는다.

  3. is_sub_active write 경로. 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 챕터를 참고하라.

  1. sub-transaction은 LIFO sub_ids 스택에 자기 MVCCID를 받는다. logtb_get_new_subtransaction_mvccid는 한 id (부모 stamped) 또는 get_two_new_mvccid를 통해 원자적으로 두 개 (부모 unstamped)를 할당하며, 항상 부모를 모든 sub-id 아래로 유지한다.
  2. 부모는 자신과 자신의 sub들의 쓰기를 본다logtb_is_current_mvccid를 통해, id를 체크한 뒤 전체 sub_ids 벡터를 선형 스캔한다 — 꼭대기만이 아니다 — 그래서 더 이른 여전히 열려 있는 sub의 쓰기도 “내 것”으로 다시 읽힌다.
  3. sub-completion은 스냅샷 fix-up이지 vacuum 이벤트가 아니다. logtb_complete_sub_mvcc는 스냅샷의 highest_completed_mvccid를 sub-id 너머로 올리고 active-set에서 클리어한다. mvcctable::complete_sub_mvccSUBTRAN 슬롯을 publish하지만 advance_oldest_active를 건너뛴다 — 열린 부모의 sub는 절대 oldest일 수 없기 때문이다.
  4. 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한다.
  5. 재시작은 log 헤더에서 테이블을 재-anchoring한다. reset_start_mvccid는 현재 status와 현재 ring 슬롯의 bit-area origin과 캐시된 lowest-active 스칼라를 log_Gl.hdr.mvcc_next_id로 설정하고, active 영역을 비워둔다 — 부팅 동안과 세 복구 체크포인트에서 single-threaded로.
  6. 잠재된 두 이슈가 미해결 질문이다 — 단일 진행 중 스냅샷 복사 동안의 풀-링 (2048) 덮어쓰기, 그리고 complete_sub_mvcc의 정보 필드 이중 할당 — 둘 다 관찰된 경로에서는 정확성에 영향을 주지 않는다.
SymbolFileLine
OR_MVCC_INSERT_ID_OFFSETsrc/base/object_representation.h483
OR_MVCC_DELETE_ID_OFFSETsrc/base/object_representation.h486
OR_MVCC_PREV_VERSION_LSA_OFFSETsrc/base/object_representation.h490
OR_GET_MVCC_FLAGsrc/base/object_representation.h548
OR_MVCC_MAX_HEADER_SIZEsrc/base/object_representation_constants.h142
OR_MVCC_MIN_HEADER_SIZEsrc/base/object_representation_constants.h145
OR_MVCC_FLAG_MASKsrc/base/object_representation_constants.h160
OR_MVCC_FLAG_VALID_INSIDsrc/base/object_representation_constants.h165
OR_MVCC_FLAG_VALID_DELIDsrc/base/object_representation_constants.h168
OR_MVCC_FLAG_VALID_PREV_VERSIONsrc/base/object_representation_constants.h171
or_mvcc_get_headersrc/base/object_representation_sr.c4237
or_mvcc_set_headersrc/base/object_representation_sr.c4296
or_mvcc_add_headersrc/base/object_representation_sr.c4381
or_mvcc_get_flagsrc/base/object_representation_sr.c4473
or_mvcc_set_flagsrc/base/object_representation_sr.c4488
or_mvcc_get_insidsrc/base/object_representation_sr.c4517
or_mvcc_set_insidsrc/base/object_representation_sr.c4544
or_mvcc_get_delidsrc/base/object_representation_sr.c4564
or_mvcc_get_chnsrc/base/object_representation_sr.c4592
or_mvcc_set_delidsrc/base/object_representation_sr.c4617
or_mvcc_set_chnsrc/base/object_representation_sr.c4638
or_mvcc_set_prev_version_lsasrc/base/object_representation_sr.c4654
or_mvcc_get_prev_version_lsasrc/base/object_representation_sr.c4680
PERF_SNAPSHOT_SATISFIES_SNAPSHOTsrc/base/perf_monitor.h238
PERF_SNAPSHOT_RECORD_INSERTED_VACUUMEDsrc/base/perf_monitor.h246
PERF_SNAPSHOT_RECORD_INSERTED_COMMITED_LOSTsrc/base/perf_monitor.h250
PERF_SNAPSHOT_RECORD_INSERTED_DELETEDsrc/base/perf_monitor.h252
PERF_SNAPSHOT_RECORD_DELETED_COMMITTED_LOSTsrc/base/perf_monitor.h257
perfmon_mvcc_snapshotsrc/base/perf_monitor.h1693
mvcc_header_size_lookupsrc/object/object_representation.c70
vacuum_data_entrysrc/query/vacuum.c104
vacuum_bootsrc/query/vacuum.c1291
vacuum_heap_pagesrc/query/vacuum.c1577
vacuum_master_task::executesrc/query/vacuum.c3002
vacuum_master_task::is_cursor_entry_ready_to_vacuumsrc/query/vacuum.c3106
vacuum_process_log_blocksrc/query/vacuum.c3251
is_not_vacuumed_and_lostsrc/query/vacuum.c7379
vacuum_rv_check_at_undosrc/query/vacuum.c7627
vacuum_is_mvccid_vacuumedsrc/query/vacuum.h271
heap_get_mvcc_headersrc/storage/heap_file.c7747
heap_attrinfo_transform_header_to_disksrc/storage/heap_file.c11937
heap_mvcc_log_insertsrc/storage/heap_file.c16371
heap_rv_mvcc_redo_insertsrc/storage/heap_file.c16442
heap_get_mvcc_rec_header_from_overflowsrc/storage/heap_file.c19541
heap_insert_adjust_recdes_headersrc/storage/heap_file.c20540
NULL_CHNsrc/storage/storage_common.h66
MVCCIDsrc/storage/storage_common.h186
MVCCID_NULLsrc/storage/storage_common.h327
MVCCID_ALL_VISIBLEsrc/storage/storage_common.h329
MVCCID_FIRSTsrc/storage/storage_common.h330
MVCCID_IS_NORMALsrc/storage/storage_common.h335
MVCCID_FORWARDsrc/storage/storage_common.h343
xlocator_upgrade_instances_domainsrc/transaction/locator_sr.c12126
log_Gl.mvcc_tablesrc/transaction/log_impl.h707
mvcc_next_idsrc/transaction/log_storage.hpp131
logtb_expand_trantablesrc/transaction/log_tran_table.c251
logtb_define_trantablesrc/transaction/log_tran_table.c366
logtb_get_number_of_total_tran_indicessrc/transaction/log_tran_table.c696
logtb_rv_assign_mvccid_for_undo_recoverysrc/transaction/log_tran_table.c1115
logtb_invalidate_snapshot_datasrc/transaction/log_tran_table.c3861
logtb_find_current_mvccidsrc/transaction/log_tran_table.c3910
logtb_get_current_mvccidsrc/transaction/log_tran_table.c3939
logtb_is_current_mvccidsrc/transaction/log_tran_table.c3972
logtb_get_mvcc_snapshotsrc/transaction/log_tran_table.c4007
logtb_complete_mvccsrc/transaction/log_tran_table.c4050
logtb_get_new_subtransaction_mvccidsrc/transaction/log_tran_table.c4547
logtb_assign_subtransaction_mvccidsrc/transaction/log_tran_table.c4578
logtb_complete_sub_mvccsrc/transaction/log_tran_table.c4593
log_tdes::lock_global_oldest_visible_mvccidsrc/transaction/log_tran_table.c6220
log_tdes::unlock_global_oldest_visible_mvccidsrc/transaction/log_tran_table.c6230
MVCC_IS_REC_INSERTER_ACTIVEsrc/transaction/mvcc.c46
MVCC_IS_REC_DELETER_ACTIVEsrc/transaction/mvcc.c49
MVCC_IS_REC_INSERTER_IN_SNAPSHOTsrc/transaction/mvcc.c52
MVCC_IS_REC_DELETER_IN_SNAPSHOTsrc/transaction/mvcc.c55
MVCC_IS_REC_INSERTED_SINCE_MVCCIDsrc/transaction/mvcc.c58
MVCC_IS_REC_DELETED_SINCE_MVCCIDsrc/transaction/mvcc.c61
mvcc_is_id_in_snapshotsrc/transaction/mvcc.c90
mvcc_is_active_idsrc/transaction/mvcc.c122
mvcc_satisfies_snapshotsrc/transaction/mvcc.c156
mvcc_is_not_deleted_for_snapshotsrc/transaction/mvcc.c280
mvcc_satisfies_vacuumsrc/transaction/mvcc.c321
mvcc_satisfies_deletesrc/transaction/mvcc.c389
mvcc_satisfies_dirtysrc/transaction/mvcc.c513
mvcc_is_mvcc_disabled_classsrc/transaction/mvcc.c628
mvcc_snapshot::copy_tosrc/transaction/mvcc.c679
mvcc_info::copy_tosrc/transaction/mvcc.c714
mvcc_rec_headersrc/transaction/mvcc.h38
MVCC_REC_HEADER_INITIALIZERsrc/transaction/mvcc.h47
MVCC_IS_HEADER_DELID_VALIDsrc/transaction/mvcc.h87
MVCC_IS_HEADER_INSID_NOT_ALL_VISIBLEsrc/transaction/mvcc.h91
MVCC_IS_HEADER_ALL_VISIBLEsrc/transaction/mvcc.h95
MVCC_IS_REC_INSERTED_BY_MEsrc/transaction/mvcc.h118
MVCC_IS_REC_DELETED_BY_MEsrc/transaction/mvcc.h122
MVCC_IS_REC_DELETED_BYsrc/transaction/mvcc.h130
MVCC_ID_PRECEDESsrc/transaction/mvcc.h141
MVCC_ID_FOLLOW_OR_EQUALsrc/transaction/mvcc.h142
MVCC_GET_PREV_VERSION_LSAsrc/transaction/mvcc.h156
mvcc_satisfies_snapshot_resultsrc/transaction/mvcc.h159
MVCC_SNAPSHOT_FUNCsrc/transaction/mvcc.h171
mvcc_snapshotsrc/transaction/mvcc.h173
mvcc_infosrc/transaction/mvcc.h196
mvcc_satisfies_delete_resultsrc/transaction/mvcc.h222
mvcc_satisfies_vacuum_resultsrc/transaction/mvcc.h232
mvcc_active_tran::mvcc_active_transrc/transaction/mvcc_active_tran.cpp31
mvcc_active_tran::initializesrc/transaction/mvcc_active_tran.cpp47
mvcc_active_tran::finalizesrc/transaction/mvcc_active_tran.cpp62
mvcc_active_tran::resetsrc/transaction/mvcc_active_tran.cpp74
mvcc_active_tran::long_tran_max_sizesrc/transaction/mvcc_active_tran.cpp99
mvcc_active_tran::bit_size_to_unit_sizesrc/transaction/mvcc_active_tran.cpp105
mvcc_active_tran::units_to_bitssrc/transaction/mvcc_active_tran.cpp111
mvcc_active_tran::units_to_bytessrc/transaction/mvcc_active_tran.cpp117
mvcc_active_tran::get_mask_ofsrc/transaction/mvcc_active_tran.cpp123
mvcc_active_tran::get_bit_offsetsrc/transaction/mvcc_active_tran.cpp129
mvcc_active_tran::get_mvccidsrc/transaction/mvcc_active_tran.cpp135
mvcc_active_tran::get_unit_ofsrc/transaction/mvcc_active_tran.cpp141
mvcc_active_tran::is_setsrc/transaction/mvcc_active_tran.cpp147
mvcc_active_tran::get_area_sizesrc/transaction/mvcc_active_tran.cpp153
mvcc_active_tran::get_bit_area_memsizesrc/transaction/mvcc_active_tran.cpp159
mvcc_active_tran::compute_highest_completed_mvccidsrc/transaction/mvcc_active_tran.cpp171
mvcc_active_tran::compute_lowest_active_mvccidsrc/transaction/mvcc_active_tran.cpp220
mvcc_active_tran::copy_tosrc/transaction/mvcc_active_tran.cpp280
mvcc_active_tran::is_activesrc/transaction/mvcc_active_tran.cpp318
mvcc_active_tran::remove_long_transactionsrc/transaction/mvcc_active_tran.cpp356
mvcc_active_tran::add_long_transactionsrc/transaction/mvcc_active_tran.cpp377
mvcc_active_tran::ltrim_areasrc/transaction/mvcc_active_tran.cpp386
mvcc_active_tran::set_bitarea_mvccidsrc/transaction/mvcc_active_tran.cpp414
mvcc_active_tran::cleanup_migrate_to_long_transationssrc/transaction/mvcc_active_tran.cpp462
mvcc_active_tran::set_inactive_mvccidsrc/transaction/mvcc_active_tran.cpp493
mvcc_active_tran::reset_start_mvccidsrc/transaction/mvcc_active_tran.cpp506
mvcc_active_tran::reset_active_transactionssrc/transaction/mvcc_active_tran.cpp517
mvcc_active_tran::check_validsrc/transaction/mvcc_active_tran.cpp525
mvcc_active_transrc/transaction/mvcc_active_tran.hpp31
mvcc_active_tran::unit_typesrc/transaction/mvcc_active_tran.hpp63
BITAREA_MAX_SIZEsrc/transaction/mvcc_active_tran.hpp65
mvcc_active_tran::BITAREA_MAX_SIZEsrc/transaction/mvcc_active_tran.hpp65
UNIT_BIT_COUNTsrc/transaction/mvcc_active_tran.hpp69
BITAREA_MAX_MEMSIZEsrc/transaction/mvcc_active_tran.hpp71
BITAREA_MAX_BITSsrc/transaction/mvcc_active_tran.hpp72
ALL_ACTIVEsrc/transaction/mvcc_active_tran.hpp74
mvcc_active_tran::ALL_ACTIVEsrc/transaction/mvcc_active_tran.hpp74
mvcc_active_tran::ALL_COMMITTEDsrc/transaction/mvcc_active_tran.hpp75
mvcc_active_tran::m_bit_areasrc/transaction/mvcc_active_tran.hpp78
mvcc_active_tran::m_bit_area_start_mvccidsrc/transaction/mvcc_active_tran.hpp80
mvcc_active_tran::m_bit_area_lengthsrc/transaction/mvcc_active_tran.hpp82
mvcc_active_tran::m_long_tran_mvccidssrc/transaction/mvcc_active_tran.hpp85
mvcc_active_tran::m_long_tran_mvccids_lengthsrc/transaction/mvcc_active_tran.hpp87
mvcc_active_tran::m_initializedsrc/transaction/mvcc_active_tran.hpp89
oldest_active_setsrc/transaction/mvcc_table.cpp92
oldest_active_getsrc/transaction/mvcc_table.cpp102
mvcc_trans_status::mvcc_trans_statussrc/transaction/mvcc_table.cpp116
mvcc_trans_status::initializesrc/transaction/mvcc_table.cpp128
mvcc_trans_status::finalizesrc/transaction/mvcc_table.cpp135
mvcctable::advance_oldest_activesrc/transaction/mvcc_table.cpp142
mvcctable::mvcctablesrc/transaction/mvcc_table.cpp164
mvcctable::initializesrc/transaction/mvcc_table.cpp184
mvcctable::alloc_transaction_lowest_activesrc/transaction/mvcc_table.cpp199
mvcctable::finalizesrc/transaction/mvcc_table.cpp212
mvcctable::build_mvcc_infosrc/transaction/mvcc_table.cpp226
mvcctable::compute_oldest_visible_mvccidsrc/transaction/mvcc_table.cpp355
mvcctable::is_activesrc/transaction/mvcc_table.cpp422
mvcctable::next_trans_status_startsrc/transaction/mvcc_table.cpp441
mvcctable::next_tran_status_finishsrc/transaction/mvcc_table.cpp455
mvcctable::complete_mvccsrc/transaction/mvcc_table.cpp465
mvcctable::complete_sub_mvccsrc/transaction/mvcc_table.cpp541
mvcctable::get_new_mvccidsrc/transaction/mvcc_table.cpp565
mvcctable::get_two_new_mvccidsrc/transaction/mvcc_table.cpp579
mvcctable::reset_transaction_lowest_activesrc/transaction/mvcc_table.cpp593
mvcctable::reset_start_mvccidsrc/transaction/mvcc_table.cpp599
mvcctable::get_global_oldest_visiblesrc/transaction/mvcc_table.cpp611
mvcctable::update_global_oldest_visiblesrc/transaction/mvcc_table.cpp617
mvcctable::lock_global_oldest_visiblesrc/transaction/mvcc_table.cpp632
mvcctable::unlock_global_oldest_visiblesrc/transaction/mvcc_table.cpp638
mvcctable::is_global_oldest_visible_lockedsrc/transaction/mvcc_table.cpp645
mvcc_trans_statussrc/transaction/mvcc_table.hpp40
mvcctablesrc/transaction/mvcc_table.hpp64
HISTORY_MAX_SIZEsrc/transaction/mvcc_table.hpp97
mvcctable::HISTORY_MAX_SIZEsrc/transaction/mvcc_table.hpp97
HISTORY_INDEX_MASKsrc/transaction/mvcc_table.hpp98
mvcctable::m_transaction_lowest_visible_mvccidssrc/transaction/mvcc_table.hpp101
mvcctable::m_current_status_lowest_active_mvccidsrc/transaction/mvcc_table.hpp104
mvcctable::m_current_trans_statussrc/transaction/mvcc_table.hpp107
m_trans_status_history_positionsrc/transaction/mvcc_table.hpp110
mvcctable::m_trans_status_history_positionsrc/transaction/mvcc_table.hpp110
mvcctable::m_trans_status_historysrc/transaction/mvcc_table.hpp111
mvcctable::m_oldest_visiblesrc/transaction/mvcc_table.hpp118
mvcctable::m_ov_lock_countsrc/transaction/mvcc_table.hpp119
  • 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.