콘텐츠로 이동

CUBRID Double Write Buffer — 코드 수준 심층 분석

이 문서의 위치: 상위 분석 문서인 cubrid-double-write-buffer.md는 설계 의도와 이론적 배경을 다룬다. 이 문서는 모든 분기와 필드를 코드 수준에서 추적한다. 각 챕터는 독립적으로 읽을 수 있으나, 순서대로 읽으면 단일 페이지가 DWB를 통과하는 전체 생명주기 — 슬롯 예약, 블록 채움, 이중 쓰기, 그리고 크래시 복구 — 를 순서대로 따라갈 수 있다.

목차:

Ch제목상태
1자료구조 지도
2Position-with-Flags 워드
3파라미터 로딩, 블록 할당, 메모리 레이아웃
4슬롯 획득과 스테이징
5해시 삽입과 블록 채움
6DWB 볼륨 및 홈으로의 블록 플러시
7데몬 플러시 드라이버와 파일 동기화 헬퍼
8페이지 버퍼 외부에서 프로듀서가 DWB를 건드리는 경우와 강제 드레인
9크래시 복구와 볼륨 재생
10구조 변경 대기 큐와 해체

이 챕터는 DWB를 구성하는 인메모리·온디스크 구조가 무엇이며, 각 필드와 포인터가 어떻게 연결되는지를 답한다. 이후 챕터들은 모두 여기서 정의하는 일곱 개의 구조체와 하나의 싱글턴을 전제로 한다. Torn-page 이론은 고수준 동반 문서 cubrid-double-write-buffer.md의 “Theoretical Background”, “Torn-page protection” 절에 있다. 모든 정의는 src/storage/double_write_buffer.cpp에 있으며, double_write_slot만 예외로 .hpp에 존재한다(caller가 직접 이름을 쓰는 유일한 타입). 인스턴스는 파일 스코프의 static이다.

1.1 전체 구조 — 누가 누구를 소유하는가

섹션 제목: “1.1 전체 구조 — 누가 누구를 소유하는가”

DWB는 고정 크기의 블록 배열이다. 각 블록은 하나의 연속된 write_buffer(유일한 실제 페이지 메모리)를 소유하며, 두 개의 병렬 뷰를 가진다: slots(페이지 하나당 DWB_SLOT 하나)와 flush_volumes_info(서로 다른 목적지 볼륨 하나당 하나). 싱글턴 dwb_Global은 블록 배열, 락-프리 VPID 해시맵, 그리고 채우기 커서와 블록별 상태를 인코딩하는 64비트 position_with_flags 워드를 소유한다(Chapter 2).

flowchart TD
  G["dwb_Global<br/>(double_write_buffer)"] --> B0["blocks[0]<br/>(double_write_block)"]
  G --> B1["blocks[1] ... blocks[num_blocks-1]"]
  G --> HM["slots_hashmap<br/>(lockfree_hashmap&lt;VPID, dwb_slots_hash_entry&gt;)"]
  G --> PWF["position_with_flags (UINT64)"]
  B0 --> WB["write_buffer<br/>(char* — 유일한 backing memory)"]
  B0 --> SL["slots[]<br/>(DWB_SLOT — write_buffer의 VIEW)"]
  B0 --> FV["flush_volumes_info[]<br/>(FLUSH_VOLUME_INFO — 목적지 볼륨별 하나)"]
  SL -. "io_page == write_buffer + i*IO_PAGESIZE" .-> WB
  HM --> HE["dwb_slots_hash_entry"]
  HE -. "slot" .-> SL

Figure 1-1. 소유/앨리어싱 그래프. 실선 화살표는 소유, 점선 화살표는 비소유 앨리어스.

1.2 DWB_SLOT (double_write_slot) — 페이지별 핸들

섹션 제목: “1.2 DWB_SLOT (double_write_slot) — 페이지별 핸들”

슬롯은 블록의 write_buffer에 스테이징된 페이지 하나에 대한 핸들이다. dwb_set_data_on_next_slot/dwb_add_page에서 caller에게 전달되며, 플러시 경로가 순회하는 단위이기도 하다.

// double_write_slot -- src/storage/double_write_buffer.hpp
struct double_write_slot
{
FILEIO_PAGE *io_page; /* <- NOT owned; points into block->write_buffer */
VPID vpid;
LOG_LSA lsa;
bool ensure_metadata;
unsigned int position_in_block;
unsigned int block_no;
};
필드역할존재 이유
io_page블록의 write_buffer에 있는 스테이징된 페이지 이미지. 빌려온 것으로, 절대 해제하지 않는다.플러시가 이중 복사 없이 페이지 바이트를 직접 쓴다.
vpid현재 보유 중인 (volid, pageid) 식별자.해시 키 + 홈 주소; io_page->prv에서 읽어 채울 때마다 덮어쓴다.
lsa스테이징된 페이지의 Log Sequence Address.같은 VPID의 두 복사본이 있을 때 “최신 우선” 판단 기준.
ensure_metadatatrue이면 볼륨 sync 시 메타데이터도 함께 플러시.볼륨 헤더는 목적지 메타데이터가 내구적이어야 한다.
position_in_block슬롯 인덱스 (0 .. num_block_pages-1). 고정값.write_buffer 내 오프셋 고정 (position_in_block * IO_PAGESIZE).
block_nodwb_Global.blocks에서 소유 블록의 인덱스. 고정값.맨 슬롯 포인터만으로 자기 블록을 찾을 수 있게 한다.

INVARIANT (slot↔buffer aliasing). 모든 블록과 슬롯 인덱스 i에서 block->slots[i].io_page == (FILEIO_PAGE *)(block->write_buffer + i * IO_PAGESIZE)가 성립한다. dwb_create_blocks에서 확립되며, 위반 시 write_buffer의 단일 블록 쓰기에서 해당 페이지가 누락되어 DWB 볼륨에서 조용히 드롭된다:

// dwb_create_blocks -- src/storage/double_write_buffer.cpp
io_page = (FILEIO_PAGE *) (blocks_write_buffer[i] + j * IO_PAGESIZE); /* <- offset == j*IO_PAGESIZE */
dwb_initialize_slot (&slots[i][j], io_page, j, i); /* <- j becomes position_in_block */

1.3 double_write_block — 플러시 단위

섹션 제목: “1.3 double_write_block — 플러시 단위”

DWB 볼륨 쓰기의 최소 단위이며, 레이아웃은 dwb_initialize_block이 설정한다.

// double_write_block -- src/storage/double_write_buffer.cpp
struct double_write_block
{
FLUSH_VOLUME_INFO *flush_volumes_info;
volatile unsigned int count_flush_volumes_info;
unsigned int max_to_flush_vdes;
pthread_mutex_t mutex;
DWB_WAIT_QUEUE wait_queue;
char *write_buffer; /* <- the ONLY owned page memory; write all pages at once */
DWB_SLOT *slots; /* <- view over write_buffer; write individual pages */
volatile unsigned int count_wb_pages;
unsigned int block_no;
volatile UINT64 version;
volatile bool all_pages_written;
};
필드역할존재 이유
flush_volumes_infoFLUSH_VOLUME_INFO 배열, 서로 다른 목적지 볼륨별 하나.페이지들이 여러 홈 볼륨에 걸쳐 있으므로 각각 한 번씩 fsync한다.
count_flush_volumes_info현재 사용 중인 항목 수. volatile.새 목적지 볼륨이 등장할수록 증가; 파일 sync 헬퍼가 읽는다.
max_to_flush_vdes최대 용량 (== num_block_pages).최악의 경우: 페이지마다 다른 볼륨.
mutex이 블록의 wait_queue 보호.플러시를 기다리는 스레드의 직렬화.
wait_queue이 블록의 플러시를 기다리는 스레드들.배압(backpressure) 구현 (Ch 8).
write_buffer유일하게 소유된 페이지 메모리num_block_pages * IO_PAGESIZE.블록 전체를 단일 I/O로 쓴다; slots[i].io_page가 이 안을 가리킨다.
slotsnum_block_pages개의 DWB_SLOT, write_buffer의 뷰.페이지별 메타데이터 + 홈 볼륨별 쓰기 핸들.
count_wb_pages채우기 수준. volatile.num_block_pages에 도달하면 블록이 꽉 찬 것; 원자적으로 증가.
block_nodwb_Global.blocks에서의 인덱스.자기 식별; 모든 슬롯의 block_no와 일치한다.
version플러시마다 증가하는 카운터. volatile.같은 슬롯의 다른 생애(incarnation)를 구별; DWB_GET_BLOCK_VERSION으로 접근.
all_pages_written모든 페이지가 홈 볼륨에 도달하면 true. volatile.드라이버에게 블록이 소진(drain)되어 재사용 가능함을 알린다.

INVARIANT (버전 내 단조 증가). 하나의 version 안에서 count_wb_pages0에서 num_block_pages로만 올라간다 (재활용 시 version 증가와 함께 리셋). 생산자가 커서를 원자적으로 전진시키므로(Ch 4), 증가 없이 역방향 이동이 발생하면 두 생산자가 같은 슬롯을 받게 된다.

flowchart LR
  BLK["double_write_block"] --> WB["write_buffer<br/>num_block_pages * IO_PAGESIZE"]
  BLK --> SLOTS["slots[] — write_buffer의 VIEW"]
  BLK --> FVI["flush_volumes_info[] — 홈 볼륨별 하나"]

Figure 1-2. 블록의 세 병렬 배열; slotswrite_buffer를 앨리어싱한다.

1.4 flush_volume_info와 FLUSH_VOLUME_STATUS

섹션 제목: “1.4 flush_volume_info와 FLUSH_VOLUME_STATUS”

블록이 DWB 볼륨에 기록된 이후, 각 페이지는 홈 볼륨으로 복사된다(각 볼륨에 fsync 한 번). 이 구조체는 볼륨별 진행 상태를 추적한다.

// flush_volume_info -- src/storage/double_write_buffer.cpp
struct flush_volume_info
{
int vdes;
volatile int num_pages;
volatile bool all_pages_written;
volatile bool metadata;
volatile FLUSH_VOLUME_STATUS flushed_status;
};
필드역할존재 이유
vdes홈 볼륨의 OS 파일 디스크립터.페이지를 쓰고 fsync할 대상 볼륨.
num_pagesvdes로 향하는 페이지 수. volatile.fsync에 앞서 완료해야 할 쓰기 횟수.
all_pages_written모든 num_pages 쓰기가 완료되면 true. volatile.게이트: 모든 쓰기가 완료된 후에만 fsync.
metadatafsync 시 메타데이터도 함께 플러시할지 여부. volatile.라우팅된 페이지들의 ensure_metadata를 OR 누적한 값.
flushed_status3-상태 내구성 마커. volatile.어느 스레드가 fsync했는지 정확히 한 번 기록.
// FLUSH_VOLUME_STATUS -- src/storage/double_write_buffer.cpp
typedef enum
{
VOLUME_NOT_FLUSHED,
VOLUME_FLUSHED_BY_DWB_FILE_SYNC_HELPER_THREAD,
VOLUME_FLUSHED_BY_DWB_FLUSH_THREAD
} FLUSH_VOLUME_STATUS;
의미
VOLUME_NOT_FLUSHED이 블록에서 아직 fsync되지 않음. 할당 시 초기 상태.
VOLUME_FLUSHED_BY_DWB_FILE_SYNC_HELPER_THREAD파일 sync 헬퍼가 CAS를 획득하여 fsync 완료.
VOLUME_FLUSHED_BY_DWB_FLUSH_THREAD플러시 스레드가 fsync 완료(헬퍼가 먼저 도달하지 못한 경우).

INVARIANT (볼륨별 블록당 fsync 정확히 한 번). 플러시 스레드와 파일 sync 헬퍼가 ATOMIC_CAS_32 (&...flushed_status, VOLUME_NOT_FLUSHED, ...)로 경쟁한다. 승자가 fsync하고, 패자는 건너뛴다. CAS 자체가 내구성·핸드오프 신호 역할을 겸한다(Ch 6–7).

1.5 dwb_slots_hash_entry와 락-프리 해시맵

섹션 제목: “1.5 dwb_slots_hash_entry와 락-프리 해시맵”

이 맵은 “VPID v가 스테이징되어 있는가, 있다면 어디에?”라는 질문에 답한다.

// dwb_slots_hash_entry -- src/storage/double_write_buffer.cpp
struct dwb_slots_hash_entry
{
VPID vpid;
DWB_SLOTS_HASH_ENTRY *stack; /* freelist link */
DWB_SLOTS_HASH_ENTRY *next; /* bucket link */
pthread_mutex_t mutex;
UINT64 del_id; /* lock-free reclaim stamp */
DWB_SLOT *slot;
dwb_slots_hash_entry () { pthread_mutex_init (&mutex, NULL); } /* <- ctor/dtor own the mutex lifecycle */
~dwb_slots_hash_entry () { pthread_mutex_destroy (&mutex); }
};
필드역할존재 이유
vpid해시 키 — 페이지 식별자.조회 시 비교 대상; 디스크립터의 키 오프셋.
stack프리 리스트용 침투적 링크.회수된 항목들이 이 링크로 연결된다.
next버킷 체인용 침투적 링크.같은 버킷의 항목들이 이 링크로 연결된다.
mutex항목별 잠금 (LF_EM_USING_MUTEX).짧은 항목별 임계 구역; ctor/dtor가 생명주기를 소유.
del_id안전한 회수를 위한 삭제 트랜잭션 ID.어떤 reader도 보유하지 않을 때까지 해제를 지연.
slot이 VPID의 페이지를 보유한 DWB_SLOT.VPID를 스테이징된 페이지로 해소하여 읽기에 제공.

slots_entry_Descriptor는 이 필드들을 오프셋으로 등록한다 — stack(프리리스트), next(버킷), del_id(회수), vpid(키), mutex, LF_EM_USING_MUTEX — 이 오프셋 정보를 바탕으로 제네릭 맵이 불투명 노드를 조작한다. typedef로 묶으면:

// dwb_hashmap_type -- src/storage/double_write_buffer.cpp
using dwb_hashmap_type = cubthread::lockfree_hashmap<VPID, dwb_slots_hash_entry>;

INVARIANT (해시 항목은 가장 최신 스테이징 복사본을 추적). VPID당 최대 하나의 살아있는 항목만 존재하며, 그 slot은 가장 최신 스테이징 이미지(가장 높은 lsa)를 가리킨다. 재추가 시 항목을 재지향하므로 dwb_read_page는 절대 오래된 이미지를 반환하지 않는다.

두 개의 침투적 리스트 구조체가 블록별·전역 대기 파킹 리스트를 구현한다(double_write_block::wait_queue, double_write_buffer::wait_queue).

// double_write_wait_queue_entry -- src/storage/double_write_buffer.cpp
struct double_write_wait_queue_entry
{
void *data;
DWB_WAIT_QUEUE_ENTRY *next;
};
// double_write_wait_queue -- src/storage/double_write_buffer.cpp
struct double_write_wait_queue
{
DWB_WAIT_QUEUE_ENTRY *head;
DWB_WAIT_QUEUE_ENTRY *tail;
DWB_WAIT_QUEUE_ENTRY *free_list;
int count;
int free_count;
};
#define DWB_WAIT_QUEUE_INITIALIZER {NULL, NULL, NULL, 0, 0}

double_write_wait_queue_entry: data — 파킹된 스레드의 웨이크업 토큰(깨우는 측이 올바른 대기자에게 신호를 보낼 수 있도록); nexthead부터 tail까지 대기자들을 FIFO로 연결하는 링크.

double_write_wait_queue:

필드역할존재 이유
head첫 번째 대기자(디큐 쪽).FIFO: 가장 오래 기다린 대기자가 먼저 깨어난다.
tail마지막 대기자(인큐 쪽).O(1) 추가를 위해.
free_list재활용된 항목 노드들.malloc 반복 호출을 피하기 위해.
count현재 살아있는 대기자 수.빠른 크기 확인.
free_countfree_list의 노드 수.재활용 풀 상태 관찰.

큐 연산은 소유 구조체의 mutex 하에서 수행된다; 해제(teardown)는 Ch 10에서 다룬다.

1.7 double_write_buffer (dwb_Global 싱글턴)

섹션 제목: “1.7 double_write_buffer (dwb_Global 싱글턴)”

파일 스코프의 인스턴스 하나가 모듈 전체를 뒷받침한다:

// dwb_Global -- src/storage/double_write_buffer.cpp
static DOUBLE_WRITE_BUFFER dwb_Global;
// double_write_buffer -- src/storage/double_write_buffer.cpp
struct double_write_buffer
{
bool logging_enabled;
DWB_BLOCK *blocks;
unsigned int num_blocks; /* power of 2 */
unsigned int num_pages; /* power of 2 */
unsigned int num_block_pages; /* power of 2 */
unsigned int log2_num_block_pages;
volatile unsigned int blocks_flush_counter;
volatile unsigned int next_block_to_flush;
pthread_mutex_t mutex;
DWB_WAIT_QUEUE wait_queue;
UINT64 volatile position_with_flags; /* <- fill cursor + per-block/state flags */
dwb_hashmap_type slots_hashmap;
int vdes;
DWB_BLOCK *volatile file_sync_helper_block;
};
필드역할존재 이유
logging_enabled상세 DWB 로깅 (dwb_Log 별칭).dwb_log_* 호출의 게이트.
blocksnum_blocks개의 DWB_BLOCK 소유 배열.실제 버퍼 본체.
num_blocks블록 수 (2의 거듭제곱).position 워드의 인덱싱/마스킹 (Ch 2).
num_pagesnum_blocks * num_block_pages (2의 거듭제곱).크기 조정/검증.
num_block_pages블록당 페이지 수 (2의 거듭제곱).슬롯 배열 길이, write_buffer 크기, 채우기 임계값.
log2_num_block_pageslog2(num_block_pages).position을 (block_no, position_in_block)으로 분리하는 시프트/마스크.
blocks_flush_counter현재 플러시 중인 블록 수. volatile.SMO 정지(quiescence, Ch 10), 강제 소진(Ch 8).
next_block_to_flush드라이버가 다음에 선택할 블록. volatile.라운드로빈: 가장 오래된 블록을 먼저 소진.
mutex전역 wait_queue 보호.구조 변경 시 파킹된 스레드 직렬화.
wait_queue구조 변경을 기다리는 스레드들.SMO 중 배압 구현.
position_with_flags64비트: 하위 = 채우기 커서, 상위 = 블록별 플래그 + 생성/변경 상태. volatile.생산자들이 CAS로 슬롯을 확보하고 생명주기 플래그를 설정 (Ch 2).
slots_hashmapVPID -> dwb_slots_hash_entry 맵.dwb_read_page에 응답하고 재스테이징 페이지를 중복 제거 (1.5).
vdesDWB 볼륨의 파일 디스크립터.블록 쓰기의 목적지; 없으면 NULL_VOLDES.
file_sync_helper_block헬퍼에게 fsync를 요청받은 블록. volatile.헬퍼로의 핸드오프; 소비되면 ATOMIC_TAS_ADDR로 NULL로 교체.

INVARIANT (싱글턴 일관성). 생성자가 모든 것을 zero/NULL로 초기화하므로(vdes (NULL_VOLDES), position_with_flags (0)), DWB를 사용하지 않는 프로세스도 유효한 dwb_Global을 가진다. 생성 여부는 blocks가 아니라 position_with_flags로 판단한다(dwb_is_created). 생성된 버퍼라면 num_pages == num_blocks * num_block_pages이고 num_block_pages == 1 << log2_num_block_pages이며, 모두 2의 거듭제곱이다 — 생성 시 단 한 번 설정되는 이 관계가 성립해야만 Chapter 2의 시프트/마스크 디코딩이 올바르게 작동한다.

  1. 실제 버퍼는 하나, 뷰는 둘. 블록은 write_buffer 하나만 소유하며, slots[]flush_volumes_info[]는 페이지를 복사하지 않고 그 위에 걸친 병렬 배열이다.
  2. 앨리어싱 불변식은 핵심 구조를 떠받친다. slots[i].io_page == write_buffer + i*IO_PAGESIZEdwb_create_blocks/dwb_initialize_slot에서 position_in_block을 키로 하여 고정된다. 이 불변식이 깨지면 설계 전체가 무너진다.
  3. DWB_SLOT은 빌려온 핸들이다. io_page는 슬롯 경로로 절대 해제되지 않으며, vpid/lsa/ensure_metadata는 재사용 시 덮어쓰고, position_in_block/block_no는 초기화 후 고정된다.
  4. 볼륨별 내구성은 3-상태 경쟁이다. flushed_status에 대한 CAS로 {플러시 스레드, 파일 sync 헬퍼} 중 정확히 하나만 각 홈 볼륨을 fsync한다.
  5. 해시맵은 최신성 인덱스이지 복사본이 아니다. slot은 가장 최신 스테이징 이미지를 가리키며, stack/next/del_id/mutex는 오프셋 디스크립터와 회수만을 위해 존재한다.
  6. position_with_flags가 락-프리 프로토콜을 한 워드에 담는다. 생산자들과 플러시 드라이버가 이 단일 원자 값으로 조율한다; Chapter 2에서 디코딩한다.
  7. dwb_Global은 항상 존재한다. 생성자가 일관된 상태로 남겨두며, 존재 여부는 position_with_flags로 읽고, 2의 거듭제곱 관계는 생성된 버퍼라면 항상 성립한다.

Double write buffer의 동시성 계약 전체가 단 하나의 64비트 필드인 dwb_Global.position_with_flags에 집약되어 있다. 슬롯 할당, 블록별 “쓰기 시작” 추적, 생성·구조 변경 라이프사이클이 모두 이 워드를 공유하며, 변경은 오직 ATOMIC_CAS_64로만, 읽기는 ATOMIC_INC_64 (&..., 0ULL)(원자적 0 덧셈을 torn-read-free 로드로 활용)로만 이루어진다. 이 챕터는 비트 레이아웃, 모든 접근자·변경자 매크로, 그리고 이 워드를 전진시키는 두 CAS 재시도 루프를 상세히 살펴본다. 왜 별도의 카운터와 락 대신 원자적 단일 워드를 사용하는지는 상위 개요 문서의 “Position-with-flags — 중앙 조정자” 절 (cubrid-double-write-buffer.md)을 참고한다.

이 필드는 volatile로 선언되어 있으며, fast path에서 유일하게 변경되는 공유 상태이다:

// struct double_write_buffer -- src/storage/double_write_buffer.cpp
UINT64 volatile position_with_flags; /* The current position in double write buffer and flags. Flags keep the
* state of each block (started, ended), create DWB status, modify DWB status. */

UINT64 하나가 마스크 상수로 명명된 세 개의 비중첩 영역으로 분할된다:

// position/flag masks -- src/storage/double_write_buffer.cpp
#define DWB_POSITION_MASK 0x000000003fffffff /* <- bits 0..29 */
#define DWB_BLOCKS_STATUS_MASK 0xffffffff00000000 /* <- bits 32..63 */
#define DWB_MODIFY_STRUCTURE 0x0000000080000000 /* <- bit 31 */
#define DWB_CREATE 0x0000000040000000 /* <- bit 30 */
#define DWB_CREATE_OR_MODIFY_MASK (DWB_CREATE | DWB_MODIFY_STRUCTURE)
#define DWB_FLAG_MASK (DWB_BLOCKS_STATUS_MASK | DWB_MODIFY_STRUCTURE | DWB_CREATE)
비트영역마스크 / 플래그설정 / 해제 주체
0..29슬롯 위치DWB_POSITION_MASK (0x3fffffff)평탄화된 슬롯 배열을 순환하는 단조 증가 ring 커서; DWB_GET_NEXT_POSITION_WITH_FLAGS가 전진시키며, num_pages에서 wrap-around.
30DWB_CREATE (0x40000000)DWB 존재 / 사용 가능생성 시 DWB_STARTS_CREATION, 해제 시 DWB_ENDS_CREATION이 설정/해제.
31DWB_MODIFY_STRUCTURE (0x80000000)리사이즈 진행 중리사이즈 시작 시 DWB_STARTS_MODIFYING_STRUCTURE, 완료 시 DWB_ENDS_MODIFYING_STRUCTURE가 설정/해제; 설정된 동안 슬롯 할당 차단.
32..63블록 상태 비트마스크DWB_BLOCKS_STATUS_MASK (0xffffffff00000000)블록당 1비트(블록 b는 비트 63 - b); 설정 = “쓰기 시작됨”. DWB_STARTS_BLOCK_WRITING이 설정, DWB_ENDS_BLOCK_WRITING이 해제.

DWB_FLAG_MASK는 30비트 position을 제외한 모든 비트, 즉 position 증가 시에도 살아남아야 하는 비트들이다(2.5절). 블록 b의 상태 비트는 (63 - b) 위치로, 블록 0이 비트 63, 블록 31이 비트 32에 위치한다. 상태 비트가 32개뿐이므로 설계상 블록 수는 최대 32개(DWB_MAX_BLOCKS, 최솟값은 DWB_MIN_BLOCKS 1)로 제한된다. (제목에 등장하는 “BLOCKS_FLUSH”는 비공식 표현이다. 이 워드에는 BLOCKS_FLUSH 플래그가 존재하지 않는다. 32비트 영역은 “쓰기 시작” 비트마스크이며, 별도의 blocks_flush_counter 정수는 position_with_flags 바깥에 위치한다.)

불변 조건 — 세 영역은 결코 겹치지 않는다. 마스크 상수의 구성상 비트 비중복이 보장된다. position은 최대 0x3fffffff(10억을 훨씬 넘음)로, 어떤 합법적인 num_pages보다도 크다(DWB는 32MB로 상한이 설정됨, 챕터 3). 따라서 position 산술 연산이 비트 30으로 올림수를 전달하는 일은 없다. 마스크가 겹친다면 position 증가 하나가 라이프사이클 비트나 상태 비트를 조용히 지워버려, 아직 플러시 중인 블록을 생산자가 덮어쓰게 된다. 이 보장은 상수만으로 이루어지며, 런타임에 재확인하지 않는다.

flowchart LR
  subgraph W["position_with_flags (UINT64)"]
    direction LR
    B["비트 63..32\n블록 상태 비트마스크\n블록 b -> 비트 63-b"]
    M["비트 31\nDWB_MODIFY_STRUCTURE"]
    C["비트 30\nDWB_CREATE"]
    P["비트 29..0\n슬롯 위치\nDWB_POSITION_MASK"]
  end

Figure 2-1. position_with_flags의 비트 분할.

2.2 Position 접근자: GET, RESET, 블록 상태 읽기

섹션 제목: “2.2 Position 접근자: GET, RESET, 블록 상태 읽기”

AND 연산으로 특정 영역을 추출하는 세 가지 매크로:

// position projections -- src/storage/double_write_buffer.cpp
#define DWB_GET_POSITION(position_with_flags) \
((position_with_flags) & DWB_POSITION_MASK) /* <- bits 0..29 only */
#define DWB_RESET_POSITION(position_with_flags) \
((position_with_flags) & DWB_FLAG_MASK) /* <- keep flags, zero position */
#define DWB_GET_BLOCK_STATUS(position_with_flags) \
((position_with_flags) & DWB_BLOCKS_STATUS_MASK) /* <- bits 32..63 only */

DWB_GET_POSITION은 순수한 ring 커서 값을 반환하고, DWB_RESET_POSITION은 그 보수로서 생성 시(2.6절) 들어오는 플래그는 유지하면서 position을 0으로 시작하는 새 구조를 만들 때 사용한다. DWB_GET_BLOCK_STATUS는 블록 상태 워드(상위 32비트)를 그대로 반환하며, 호출 측에서는 절대 비트별로 해석하지 않는다. 두 가지 용도는 모두 전체 영역 제로 확인이다: dwb_starts_structure_modification 말미의 사후-플러시 assert (DWB_GET_BLOCK_STATUS (...) == 0)(리사이즈 전 모든 블록이 비워져야 함)과, dwb_flush_force의 “플러시할 대상이 있는가?” 조기 탈출 if (DWB_GET_BLOCK_STATUS (...) == 0). 비트별 검사는 DWB_IS_BLOCK_WRITE_STARTED(2.4절)로 수행한다.

2.3 Position을 블록 번호와 블록 내 오프셋으로 디코딩

섹션 제목: “2.3 Position을 블록 번호와 블록 내 오프셋으로 디코딩”

raw position은 모든 슬롯에 걸친 평탄 인덱스다. 블록당 page 수가 2의 거듭제곱이므로, 블록 번호는 상위 부분, 블록 내 오프셋은 하위 부분이 되며 log2_num_block_pages를 기준으로 분할된다. 디코딩 매크로 둘:

// position decode -- src/storage/double_write_buffer.cpp
#define DWB_GET_BLOCK_NO_FROM_POSITION(position_with_flags) \
((unsigned int) DWB_GET_POSITION (position_with_flags) >> (DWB_LOG2_BLOCK_NUM_PAGES))
#define DWB_GET_POSITION_IN_BLOCK(position_with_flags) \
((DWB_GET_POSITION (position_with_flags)) & (DWB_BLOCK_NUM_PAGES - 1))

DWB_LOG2_BLOCK_NUM_PAGES는 생성 시 단 한 번 계산되는 dwb_Global.log2_num_block_pages이다:

// dwb_create_internal -- src/storage/double_write_buffer.cpp
dwb_Global.log2_num_block_pages = (unsigned int) (log ((float) num_block_pages) / log ((float) 2));

그 로그값만큼 오른쪽으로 shift하면 블록 인덱스가 나오고, DWB_BLOCK_NUM_PAGES - 1 (하위 비트 전체가 1인 마스크, num_block_pages가 2의 거듭제곱임을 이용 — 챕터 3)과 AND하면 블록 내 오프셋이 나온다. unsigned int 캐스트는 손실이 없으며(마스크 적용 후 position은 30비트 이내), shift 연산을 32비트 산술 내에서 처리한다. 할당 루프는 두 값을 읽은 뒤 current_block_no < DWB_NUM_TOTAL_BLOCKS && position_in_current_block < DWB_BLOCK_NUM_PAGES를 assert한다.

블록 b의 “쓰기 시작” 비트(63 - block_no 위치)를 읽고 전환하는 네 가지 매크로:

// block-status bit operators -- src/storage/double_write_buffer.cpp
#define DWB_IS_BLOCK_WRITE_STARTED(position_with_flags, block_no) \
(assert (block_no < DWB_MAX_BLOCKS), ((position_with_flags) & (1ULL << (63 - (block_no)))) != 0)
#define DWB_IS_ANY_BLOCK_WRITE_STARTED(position_with_flags) \
(((position_with_flags) & DWB_BLOCKS_STATUS_MASK) != 0)
#define DWB_STARTS_BLOCK_WRITING(position_with_flags, block_no) \
(assert (block_no < DWB_MAX_BLOCKS), (position_with_flags) | (1ULL << (63 - (block_no))))
#define DWB_ENDS_BLOCK_WRITING(position_with_flags, block_no) \
(assert (DWB_IS_BLOCK_WRITE_STARTED (position_with_flags, block_no)), \
(position_with_flags) & ~(1ULL << (63 - (block_no))))

DWB_IS_BLOCK_WRITE_STARTED(w,b)는 비트 63-b를 검사하고, DWB_IS_ANY_BLOCK_WRITE_STARTED(w)는 전체 영역을 검사한다. DWB_STARTS_BLOCK_WRITING / DWB_ENDS_BLOCK_WRITING은 해당 비트를 설정/해제한 워드를 반환한다. 세 매크로는 쉼표 연산자로 assert를 내장한다(먼저 assert를 평가하고, 그 다음 비트 연산 결과를 값으로 반환): STARTS/IS 쌍은 block_no < DWB_MAX_BLOCKS를 보호하고(범위 초과 시 position/flag 영역으로 shift가 침범), ENDS는 비트가 이미 설정되어 있음을 assert하여 이중 해제를 잡아낸다. 네 매크로 모두 순수(pure) 함수로, CAS는 호출 측에서 처리한다.

블록의 첫 번째 슬롯을 차지하는 생산자는 STARTS 비트 설정과 커서 전진을 동일하게 공개된 하나의 워드에 담아야 한다. 할당자는 CAS 이전에 이 복합 값을 구성한다 — 먼저 DWB_STARTS_BLOCK_WRITING, 그 위에 DWB_GET_NEXT_POSITION_WITH_FLAGS(2.5절)를 적용하여, CAS 한 번으로 두 변경을 동시에 공개한다:

// dwb_acquire_next_slot -- src/storage/double_write_buffer.cpp
/* First write in the current block. */
assert (!DWB_IS_BLOCK_WRITE_STARTED (current_position_with_flags, current_block_no));
current_position_with_block_write_started =
DWB_STARTS_BLOCK_WRITING (current_position_with_flags, current_block_no); /* <- set status bit */
new_position_with_flags =
DWB_GET_NEXT_POSITION_WITH_FLAGS (current_position_with_block_write_started); /* <- then advance cursor */

불변 조건 — 블록의 상태 비트는 플러시 사이클 하나 동안 설정 상태를 유지한다. 블록의 첫 슬롯이 할당될 때(DWB_STARTS_BLOCK_WRITING) 설정되고, 블록이 플러시된 후에만(DWB_ENDS_BLOCK_WRITING, 챕터 6) 해제된다. 설정 상태에서 할당자가 첫 슬롯 위치로 wrap-around하면(position_in_current_block == 0), DWB_IS_BLOCK_WRITE_STARTED가 true를 반환하고 할당자는 대기한다:

// dwb_acquire_next_slot -- src/storage/double_write_buffer.cpp
if (position_in_current_block == 0)
{
if (DWB_IS_BLOCK_WRITE_STARTED (current_position_with_flags, current_block_no))
{
if (can_wait == false) { return NO_ERROR; } /* <- non-blocking caller bails out */
error_code = dwb_wait_for_block_completion (thread_p, current_block_no);
// ... condensed: ER_CSS_PTHREAD_COND_TIMEDOUT also retries ...
goto start; /* <- retry the whole allocation after the wait */
}

이 비트가 조기에 해제되거나 겹치는 마스크로 소실된다면, 아직 디스크 DWB 볼륨에 도달하지 않은 블록의 페이지를 생산자가 덮어쓰게 되어 torn-page 보호가 무력화된다.

2.5 Next-position 매크로와 할당 CAS 루프

섹션 제목: “2.5 Next-position 매크로와 할당 CAS 루프”

DWB_GET_NEXT_POSITION_WITH_FLAGS는 커서를 1 전진시키되, num_pages에서 wrap-around하며 모든 플래그 비트를 그대로 유지한다:

// DWB_GET_NEXT_POSITION_WITH_FLAGS -- src/storage/double_write_buffer.cpp
#define DWB_GET_NEXT_POSITION_WITH_FLAGS(position_with_flags) \
((DWB_GET_POSITION (position_with_flags)) == (DWB_NUM_TOTAL_PAGES - 1) \
? ((position_with_flags) & DWB_FLAG_MASK) : ((position_with_flags) + 1))

두 분기(Figure 2-2): num_pages - 1에서의 wrap 분기position_with_flags & DWB_FLAG_MASK(position을 0으로, 플래그 유지)를 반환하고, 그 외의 increment 분기position_with_flags + 1을 반환한다. position이 비트 30보다 훨씬 아래에 있으므로 하위 비트만 변경된다.

flowchart TD
  S["DWB_GET_NEXT_POSITION_WITH_FLAGS(w)"] --> Q{"DWB_GET_POSITION(w) == num_pages - 1 ?"}
  Q -->|예, 마지막 슬롯| WRAP["w AND DWB_FLAG_MASK 반환\nposition -> 0, 플래그 유지"]
  Q -->|아니오| INC["w + 1 반환\nposition 전진, 플래그 불변"]

Figure 2-2. DWB_GET_NEXT_POSITION_WITH_FLAGS의 두 분기.

불변 조건 — 플래그 비트는 increment와 wrap 양쪽 모두에서 살아남는다. DWB_FLAG_MASKDWB_POSITION_MASK의 정확한 보수이므로, create 비트·구조 변경 비트·상태 비트는 어느 경로에서도 변경되지 않은 채 나온다. 이 덕분에 CAS 하나로 커서 전진과 위에서 설정한 상태 비트를 동시에 공개할 수 있다. wrap 분기가 순수한 0을 반환한다면 DWB_CREATE와 모든 상태 비트가 지워져 버퍼가 사라진 것으로 생산자가 오인한다.

이 매크로는 dwb_acquire_next_slot할당 CAS 루프, 즉 이 워드에 대한 첫 번째 재시도 루프에 사용된다. 워드는 ATOMIC_INC_64 (&dwb_Global.position_with_flags, 0ULL)로 읽히고, 조기 탈출 및 첫 슬롯/비첫 슬롯 분기(2.4절)가 new_position_with_flags를 구성하며, 단일 CAS로 커밋한다. CAS에 실패하면 goto start로 돌아가 재로드한다:

// dwb_acquire_next_slot -- src/storage/double_write_buffer.cpp
start:
current_position_with_flags = ATOMIC_INC_64 (&dwb_Global.position_with_flags, 0ULL); /* <- atomic load */
if (DWB_NOT_CREATED_OR_MODIFYING (current_position_with_flags))
{ /* ... 2.6 early-out: modifying -> wait, not created -> bail ... */ }
// ... compute new_position_with_flags via 2.4 ...
if (!ATOMIC_CAS_64 (&dwb_Global.position_with_flags, current_position_with_flags, new_position_with_flags))
{
goto start; /* <- another thread won the race; reload and retry */
}

2.6 라이프사이클 플래그 연산자와 구조 변경 CAS 루프

섹션 제목: “2.6 라이프사이클 플래그 연산자와 구조 변경 CAS 루프”

비트 30과 31은 DWB 라이프사이클을 담당한다. 각각 설정/해제/검사 트리오와 조합 predicate를 갖는다:

// lifecycle flag operators -- src/storage/double_write_buffer.cpp
#define DWB_STARTS_MODIFYING_STRUCTURE(position_with_flags) \
((position_with_flags) | DWB_MODIFY_STRUCTURE)
#define DWB_ENDS_MODIFYING_STRUCTURE(position_with_flags) \
(assert (DWB_IS_MODIFYING_STRUCTURE (position_with_flags)), (position_with_flags) & ~DWB_MODIFY_STRUCTURE)
#define DWB_IS_MODIFYING_STRUCTURE(position_with_flags) \
(((position_with_flags) & DWB_MODIFY_STRUCTURE) != 0)
#define DWB_STARTS_CREATION(position_with_flags) \
((position_with_flags) | DWB_CREATE)
#define DWB_ENDS_CREATION(position_with_flags) \
(assert (DWB_IS_CREATED (position_with_flags)), (position_with_flags) & ~DWB_CREATE)
#define DWB_IS_CREATED(position_with_flags) \
(((position_with_flags) & DWB_CREATE) != 0)
#define DWB_NOT_CREATED_OR_MODIFYING(position_with_flags) \
(((position_with_flags) & DWB_CREATE_OR_MODIFY_MASK) != DWB_CREATE)

생성 트리오는 비트 30에, 변경 트리오는 비트 31에 작용한다. set 매크로는 단순 OR, clear 매크로는 비트가 설정되어 있음을 assert(begin/end 비대칭 쌍을 잡아냄)한 뒤 보수와 AND한다. DWB_NOT_CREATED_OR_MODIFYING은 두 라이프사이클 비트를 함께 마스킹하여 결과가 정확히 DWB_CREATE인지 검사한다. “생성됨, 변경 중이 아님”일 때만 false이고, DWB가 없거나(비트 30 해제) 리사이즈 중이면(비트 31 설정) true이다. 이것이 할당 및 플러시 경로 최상단의 조기 탈출 guard이다 (if (DWB_NOT_CREATED_OR_MODIFYING (...)) { ... skip ... }). dwb_acquire_next_slot에서 이 guard가 발동하면 세 가지로 분기한다: DWB_IS_MODIFYING_STRUCTURE가 true이면 dwb_wait_for_strucure_modification으로 대기 후 goto start 재시도; 그렇지 않으면 DWB가 삭제된 경우로, 쓰기가 시작된 블록이 남아 있으면(DWB_IS_ANY_BLOCK_WRITE_STARTED — 데이터 유실) ER_DWB_DISABLED, 없으면 조용한 NO_ERROR를 반환한다. 마지막 elseassert (false)이다(마스크 비교가 이 경우를 배제하기 때문).

생성은 DWB_RESET_POSITIONDWB_STARTS_CREATION → CAS 순으로 DWB_CREATE를 공개하여 position 0에서 create 비트가 켜진 새 구조를 시작한다(실패 시 assert(false) — 생성은 단일 스레드):

// dwb_create_internal -- src/storage/double_write_buffer.cpp
new_position_with_flags = DWB_RESET_POSITION (*current_position_with_flags);
new_position_with_flags = DWB_STARTS_CREATION (new_position_with_flags);
if (!ATOMIC_CAS_64 (&dwb_Global.position_with_flags, *current_position_with_flags, new_position_with_flags))
{
assert (false); /* <- single-threaded; a race here is a bug */
}

dwb_starts_structure_modification구조 변경 CAS 루프는 이 워드에 대한 두 번째이자 마지막 실질적인 재시도 루프다. 워드를 원자적으로 읽고, 이미 리사이즈가 진행 중이면 반환하며(리사이저는 동시에 하나만 허용 — 대기 없이 ER_FAILED 반환), DWB_MODIFY_STRUCTURE를 OR한 뒤 CAS에 성공할 때까지 스핀한다:

// dwb_starts_structure_modification -- src/storage/double_write_buffer.cpp
do
{
local_current_position_with_flags = ATOMIC_INC_64 (&dwb_Global.position_with_flags, 0ULL);
if (DWB_IS_MODIFYING_STRUCTURE (local_current_position_with_flags))
{
return ER_FAILED; /* <- only one thread may modify the structure */
}
new_position_with_flags = DWB_STARTS_MODIFYING_STRUCTURE (local_current_position_with_flags);
}
while (!ATOMIC_CAS_64 (&dwb_Global.position_with_flags, local_current_position_with_flags, new_position_with_flags));
// ... condensed: drain in-flight flushers, flush all started blocks, then assert status==0 ...

불변 조건 — 마스크 비교 하나로 “영업 중”과 그 외 모든 상태를 구분한다. DWB_NOT_CREATED_OR_MODIFYING은 비트 31을 비트 30과 함께 검사한다. 위 루프가 DWB_CREATE를 해제하지 않고 DWB_MODIFY_STRUCTURE를 설정하면, 마스킹 결과는 DWB_CREATE | DWB_MODIFY_STRUCTURE가 되어 != DWB_CREATE로 배제된다. 별도의 워드였다면 생산자가 낡은 “생성됨” 상태를 보고 절반만 재구성된 버퍼에 할당할 수 있다. 단일 리사이저 보장은 무조건적인 DWB_STARTS_MODIFYING_STRUCTURE OR를 안전하게 만드는 요소이기도 하다. 루프 본문의 DWB_IS_MODIFYING_STRUCTURE 검사가 CAS로 재검증되므로, 두 스레드가 동시에 set에 성공하는 일은 없다.

  1. dwb_Global.position_with_flagsUINT64 하나를 세 개의 비중첩 영역으로 분할한다: 30비트 ring position(DWB_POSITION_MASK), 두 개의 라이프사이클 플래그 (DWB_CREATE 비트 30, DWB_MODIFY_STRUCTURE 비트 31), 상위 워드의 32비트 블록별 “쓰기 시작” 비트마스크(DWB_BLOCKS_STATUS_MASK). 읽기는 ATOMIC_INC_64 (&..., 0ULL), 쓰기는 오직 ATOMIC_CAS_64로만 이루어진다.
  2. 블록 b의 상태 비트는 (63 - b) 위치에 있으며, 블록 수를 DWB_MAX_BLOCKS = 32로 하드 캡한다. raw position은 log2_num_block_pages 기준으로 shift right / 마스크 (num_block_pages - 1)하여 (block_no, position_in_block)으로 디코딩되며, 2의 거듭제곱 크기에 의존한다.
  3. DWB_GET_NEXT_POSITION_WITH_FLAGSnum_pages - 1에서 w & DWB_FLAG_MASK로 wrap-around하여 position을 0으로, 플래그를 유지하며, 그 외에는 w + 1을 반환한다. 어느 경우든 플래그/상태 비트는 살아남으므로, CAS 하나로 커서 전진과 상태 비트 설정을 원자적으로 공개할 수 있다. 할당자는 커밋 전에 DWB_STARTS_BLOCK_WRITINGDWB_GET_NEXT_POSITION_WITH_FLAGS를 순서대로 구성한다.
  4. 이 워드를 변경하는 CAS 재시도 루프는 두 개다: dwb_acquire_next_slot의 할당 루프(CAS 실패 시 goto start, 첫 슬롯 대기는 dwb_wait_for_block_completion, 변경 중/미생성 조기 탈출 포함)와, DWB_MODIFY_STRUCTURE를 설정하는 dwb_starts_structure_modificationdo/while (!ATOMIC_CAS_64) 루프(이미 리사이즈 중이면 ER_FAILED 반환). 생성은 재시도 없는 단일 CAS(assert(false) on loss).
  5. 블록 상태 비트는 블록의 첫 슬롯이 확보될 때 설정되고 블록 플러시 후에만 해제된다. clear/end 매크로는 먼저 “설정됨” predicate를 assert하며, 모든 변경자 매크로는 순수 함수로 CAS를 호출 측에 위임한다.
  6. DWB_NOT_CREATED_OR_MODIFYING은 두 라이프사이클 비트를 DWB_CREATE에 대한 단일 마스크 비교로 통합하여, DWB가 없거나 리사이즈 중일 때 fast path를 차단한다. DWB_GET_BLOCK_STATUS는 전체 영역 제로 확인(리사이즈 경로의 사후 배수 assert, dwb_flush_force의 “플러시 대상 있음?” 판단)에만 사용되며, 비트별로는 절대 해석하지 않는다.

Chapter 3: 파라미터 로딩, 블록 할당, 메모리 레이아웃

섹션 제목: “Chapter 3: 파라미터 로딩, 블록 할당, 메모리 레이아웃”

독자 질문: Chapter 1의 구조체와 Chapter 2의 position word를 전제로 할 때, DWB는 어떻게 크기가 결정되고, 할당되고, 디스크에 포맷되고, 부트 과정에 연결되는가? 두 튜너블(PRM_ID_DWB_SIZE, PRM_ID_DWB_BLOCKS)에서 시작해 malloc된 쓰기 버퍼, slot/io_page 앨리어싱, 온디스크 포맷, 해제까지 모든 분기를 추적한다. 복구 시점의 블록 재구성은 범위 밖이다(Chapter 8). double-write buffer가 존재하는지는 cubrid-double-write-buffer.md § “Why a double write buffer”를 참조한다.

3.1 클램프 상수와 크기 결정 계약

섹션 제목: “3.1 클램프 상수와 크기 결정 계약”
// sizing constants -- src/storage/double_write_buffer.cpp
#define DWB_SLOTS_HASH_SIZE 1000
#define DWB_SLOTS_FREE_LIST_SIZE 100
#define DWB_MIN_SIZE (512 * 1024) /* 512 KB floor */
#define DWB_MAX_SIZE (32 * 1024 * 1024) /* 32 MB ceiling */
#define DWB_MIN_BLOCKS 1
#define DWB_MAX_BLOCKS 32

두 튜너블은 모두 2의 거듭제곱으로 클램프되어, num_pages = size / IO_PAGESIZEnum_block_pages = num_pages / num_blocks가 정확히 2의 거듭제곱이 되도록 보장한다. 이로써 런타임이 log2_num_block_pages(§3.7)를 캐싱해 나머지 연산(modulo) 대신 비트 마스크 시프트를 쓸 수 있다. 세 함수가 협력한다: dwb_power2_ceil, dwb_load_buffer_size, dwb_load_block_count.

3.2 dwb_power2_ceil — 클램프 후 올림 반올림

섹션 제목: “3.2 dwb_power2_ceil — 클램프 후 올림 반올림”
// dwb_power2_ceil -- src/storage/double_write_buffer.cpp
if (*p_value < min) { *p_value = min; } /* branch A: below floor -> snap to min */
else if (*p_value > max) { *p_value = max; } /* branch B: above ceiling -> snap to max */
else if (!IS_POWER_OF_2 (*p_value)) { /* branch C: in range, not power of 2 */
limit = min << 1;
while (limit < *p_value) limit = limit << 1; /* shift up until >= value -> round UP */
*p_value = limit;
}
/* branch D (implicit): in range AND already power of 2 -> untouched */
assert (IS_POWER_OF_2 (*p_value));
assert (*p_value >= min && *p_value <= max);

A/B는 경계 상수(둘 다 2의 거듭제곱)로 클램프하고, C는 min << 1에서 시작해 왼쪽으로만 시프트하므로 올림(round-up) 방식이며 max를 초과하지 않는다. D는 이미 2의 거듭제곱인 값을 건드리지 않는다.

Invariant 3-A (클램프 범위 내 2의 거듭제곱). 반환 후 *p_value[min, max] 범위의 2의 거듭제곱임이 assert로 보장된다. 이 불변식이 깨지면 num_block_pages가 2의 거듭제곱이 아니게 되어 log2_num_block_pages가 유효한 시프트 폭으로 기능하지 못한다.

flowchart TD
  S["dwb_power2_ceil(min,max,*v)"] --> A{"*v < min?"}
  A -->|yes| AA["*v = min"] --> Z["assert: 범위 내 2의 거듭제곱"]
  A -->|no| B{"*v > max?"}
  B -->|yes| BB["*v = max"] --> Z
  B -->|no| C{"IS_POWER_OF_2?"}
  C -->|yes| Z
  C -->|no| CC["limit=min<<1; while limit<*v: limit<<=1; *v=limit"] --> Z
Figure 3-2-1 — dwb_power2_ceil의 네 가지 분기

3.3 dwb_load_buffer_sizedwb_load_block_count — 0은 비활성화 게이트

섹션 제목: “3.3 dwb_load_buffer_size와 dwb_load_block_count — 0은 비활성화 게이트”
// dwb_load_buffer_size -- src/storage/double_write_buffer.cpp
assert (IS_POWER_OF_2 (DWB_MIN_SIZE) && IS_POWER_OF_2 (DWB_MAX_SIZE)); /* constants sane */
*p_double_write_buffer_size = prm_get_integer_value (PRM_ID_DWB_SIZE);
if (*p_double_write_buffer_size == 0)
return false; /* 0 -> caller skips DWB entirely */
dwb_power2_ceil (DWB_MIN_SIZE, DWB_MAX_SIZE, p_double_write_buffer_size);
return true;

파라미터를 읽어 0이면 DWB 비활성화(return false), 그렇지 않으면 dwb_power2_ceil로 클램프하고 return true를 반환한다. dwb_load_block_countPRM_ID_DWB_BLOCKS / DWB_MIN_BLOCKS / DWB_MAX_BLOCKS를 대상으로 구조적으로 동일하다.

Invariant 3-B (0은 원자적 비활성화). 두 파라미터 중 하나라도 0이면 버퍼 전체가 비활성화된다. 호출 측의 단락 평가(short-circuit) ||(!dwb_load_buffer_size(...) || !dwb_load_block_count(...))로 인해, size가 0이면 block-count 파라미터는 읽히지 않으며 함수는 아무것도 할당하지 않고 NO_ERROR를 반환한다 — 절반만 초기화된 상태가 남지 않는다.

3.4 dwb_initialize_slot — 필드별 초기화

섹션 제목: “3.4 dwb_initialize_slot — 필드별 초기화”

dwb_initialize_slotDWB_SLOT(페이지당 디스크립터)을 backing io_page에 연결한다:

// dwb_initialize_slot -- src/storage/double_write_buffer.cpp
slot->io_page = io_page;
if (io_page != NULL) { /* defensive; always true here */
VPID_SET (&slot->vpid, io_page->prv.volid, io_page->prv.pageid); /* seed from page header */
LSA_COPY (&slot->lsa, &io_page->prv.lsa);
}
slot->position_in_block = position_in_block;
slot->block_no = block_no;

double_write_slot은 여섯 개의 필드를 가지며, 이 함수에서는 다섯 개를 설정한다(나머지 하나인 ensure_metadatamemset-0 상태 그대로 유지된다):

필드초기화 시 역할 / 존재 이유
io_page호출자가 공급한 블록 쓰기 버퍼 내 앨리어스(§3.6) — 슬롯이 페이지 바이트를 참조하는 창으로, 단일 페이지 쓰기를 가능하게 함
vpidio_page->prv.{volid,pageid}에서 시드 (생성 시 둘 다 -1) — 홈 페이지를 식별; 스테이징마다 덮어씀
lsaio_page->prv.lsa에서 복사 (생성 시 NULL_LSA) — 순서 정렬 및 slots-hash 최신성 검사에 활용
ensure_metadata여기서 설정하지 않음; dwb_create_blocksmemset으로 0 유지. 플러시 시 볼륨 헤더 fsync 필요 여부 플래그; stage/flush 시점에 설정됨(Ch 4–6)
position_in_blockj, 블록 내 인덱스 — 안정적인 식별자; 슬롯을 IO_PAGESIZE 오프셋에 매핑
block_noi, 소유 블록 인덱스 — 블록으로 가는 역방향 포인터

생성 시점의 vpid/lsa는 플레이스홀더다(fileio_initialize_res가 헤더를 0으로 초기화). 실제 페이지가 스테이징될 때 덮어써진다(Chapter 4).

3.5 dwb_initialize_block — 필드별 초기화

섹션 제목: “3.5 dwb_initialize_block — 필드별 초기화”
// dwb_initialize_block -- src/storage/double_write_buffer.cpp
block->flush_volumes_info = flush_volumes_info;
block->count_flush_volumes_info = count_flush_volumes_info; /* 0 at create */
block->max_to_flush_vdes = max_to_flush_vdes; /* = num_block_pages */
pthread_mutex_init (&block->mutex, NULL);
dwb_init_wait_queue (&block->wait_queue);
block->write_buffer = write_buffer; block->slots = slots;
block->count_wb_pages = count_wb_pages; /* 0 at create */
block->block_no = block_no; block->version = 0; block->all_pages_written = false;
필드초기화 시 역할 / 존재 이유
flush_volumes_info블록당 FLUSH_VOLUME_INFO[num_block_pages] 배열 — 플러시 중 접근되는 개별 홈 볼륨마다 하나의 엔트리
count_flush_volumes_info0 — 현재 채우기 사이클에서 누적된 볼륨 수의 라이브 카운트
max_to_flush_vdesnum_block_pages — 하드 상한; 최악의 경우 모든 페이지가 서로 다른 볼륨을 가리켜도 수용
mutex새로 초기화 — 이 블록의 wait queue와 카운터를 보호
wait_queue비어 있음 — 이 블록의 플러시를 기다리는 스레드가 대기하는 곳
write_buffer연속된 num_block_pages * IO_PAGESIZE malloc — 단일 벌크 쓰기를 뒷받침
slotsDWB_SLOT[num_block_pages] 배열 — write_buffer를 앨리어싱하는 페이지당 디스크립터
count_wb_pages0 — 지금까지 스테이징된 페이지 수; 채우기 커서
block_noi — 전역적으로 사용되는 블록 식별자
version0 — 단조 증가하는 플러시 세대 카운터(Ch 2)
all_pages_writtenfalse — 완전히 스테이징되어 플러시 준비가 되면 true로 설정

Invariant 3-C (flush-info 배열은 과잉 프로비저닝됨). max_to_flush_vdes == num_block_pages이므로 모든 페이지가 서로 다른 볼륨을 대상으로 하더라도 count_flush_volumes_info가 경계를 초과하지 않는다. 런타임은 경계 검사 없이 flush_volumes_info[count_flush_volumes_info++]에 추가하며, 이 크기 결정에 의존한다.

3.6 dwb_create_blocks — 할당 엔진과 앨리어싱 기법

섹션 제목: “3.6 dwb_create_blocks — 할당 엔진과 앨리어싱 기법”

블록 배열을 할당하고, 블록마다 연속된 쓰기 버퍼, slots 배열, flush-volumes 배열을 할당한 뒤, 각 슬롯의 io_page를 쓰기 버퍼 내부에 앨리어싱한다.

// dwb_create_blocks -- src/storage/double_write_buffer.cpp
STATIC_INLINE int
dwb_create_blocks (THREAD_ENTRY *thread_p, unsigned int num_blocks, unsigned int num_block_pages, DWB_BLOCK **p_blocks)
{
char *blocks_write_buffer[DWB_MAX_BLOCKS]; /* fixed-size scratch, NULL-inited */
FLUSH_VOLUME_INFO *flush_volumes_info[DWB_MAX_BLOCKS];
DWB_SLOT *slots[DWB_MAX_BLOCKS];
// ... NULL-init all three scratch arrays over DWB_MAX_BLOCKS ...
blocks = (DWB_BLOCK *) malloc (num_blocks * sizeof (DWB_BLOCK));
if (blocks == NULL) goto exit_on_error;
memset (blocks, 0, num_blocks * sizeof (DWB_BLOCK));
block_buffer_size = num_block_pages * IO_PAGESIZE;
// ... pass 1: per block, blocks_write_buffer[i] = malloc(block_buffer_size); NULL -> exit_on_error; memset 0 ...
// ... pass 2: per block, slots[i] = malloc(num_block_pages*sizeof DWB_SLOT); NULL -> exit_on_error; memset 0 ...
// ... pass 3: per block, flush_volumes_info[i] = malloc(num_block_pages*sizeof FLUSH_VOLUME_INFO); NULL -> err ...
for (i = 0; i < num_blocks; i++) { /* pass 4: wire it together */
for (j = 0; j < num_block_pages; j++) {
io_page = (FILEIO_PAGE *) (blocks_write_buffer[i] + j * IO_PAGESIZE); /* <- ALIAS, not malloc */
fileio_initialize_res (thread_p, io_page, IO_PAGESIZE); /* zero the page header */
dwb_initialize_slot (&slots[i][j], io_page, j, i);
}
dwb_initialize_block (&blocks[i], i, 0, blocks_write_buffer[i], slots[i], flush_volumes_info[i], 0, num_block_pages);
}
*p_blocks = blocks;
return NO_ERROR;
exit_on_error:
for (i = 0; i < DWB_MAX_BLOCKS; i++) { /* free slots[i], blocks_write_buffer[i], flush_volumes_info[i] if non-NULL */ }
if (blocks != NULL) free_and_init (blocks);
return error_code;
}

핵심 동작은 pass 4의 앨리어싱이다: 각 slots[i][j].io_page는 연속 버퍼 내 write_buffer + j * IO_PAGESIZE를 가리키는 포인터로, 블록이 단일 벌크 write로 플러시되면서도 페이지 단위로 주소 지정이 가능하다. fileio_initialize_res는 각 FILEIO_PAGE 헤더를 깨끗한 초기 상태로 만든다(pageid/volid = -1, NULL LSA, zero ptype/pflag/reserves/tde_nonce). 이 값은 플러시 전에 덮어써진다. Figure 3-6-1은 분기를 추적한다. DWB_MAX_BLOCKS 크기로 NULL 초기화된 스크래치 배열 덕분에 에러 분기에서 일관되게 해제할 수 있으며, *p_blocks는 완전 성공 시에만 할당된다.

Invariant 3-D (블록 하나 = 연속 버퍼 하나). 모든 slots[i][j].io_pageblocks_write_buffer[i]의 오프셋 j*IO_PAGESIZE 지점을 정확히 가리키므로, block->write_buffer는 유효한 단일 I/O 벡터가 된다. 페이지를 별도로 malloc하면 Chapter 6의 DWB 볼륨 벌크 쓰기가 깨진다.

flowchart TD
  A["malloc blocks[]"] -->|NULL| ERR["exit_on_error"]
  A -->|ok| B["pass1: 블록마다 write_buffer malloc"]
  B -->|NULL 발생| ERR
  B -->|ok| C["pass2: 블록마다 slots malloc"]
  C -->|NULL 발생| ERR
  C -->|ok| D["pass3: 블록마다 flush_volumes_info malloc"]
  D -->|NULL 발생| ERR
  D -->|ok| E["pass4: io_page 앨리어싱, slot 초기화, block 초기화"]
  E --> F["*p_blocks = blocks; return NO_ERROR"]
  ERR --> G["NULL 아닌 scratch[i] 전체 해제, blocks 해제, error_code 반환"]
Figure 3-6-1 — dwb_create_blocks의 할당 패스와 단일 에러 분기

3.6.1 dwb_finalize_block — 블록별 해제 대응 함수

섹션 제목: “3.6.1 dwb_finalize_block — 블록별 해제 대응 함수”
// dwb_finalize_block -- src/storage/double_write_buffer.cpp
if (block->slots != NULL) free_and_init (block->slots);
if (block->write_buffer != NULL) free_and_init (block->write_buffer); /* frees the aliased pages too */
if (block->flush_volumes_info != NULL) free_and_init (block->flush_volumes_info);
dwb_destroy_wait_queue (&block->wait_queue, &block->mutex);
pthread_mutex_destroy (&block->mutex);

슬롯의 io_pagewrite_buffer 내부를 앨리어싱하므로, 페이지를 별도로 해제할 필요가 없다write_buffer를 해제하면 num_block_pages 전체가 한 번에 회수된다(Inv 3-D의 해제 측 대칭). wait queue 비우기와 mutex 해제는 마지막에 수행된다.

3.7 dwb_create_internal — 크기 결정, 포맷, 전역 연결

섹션 제목: “3.7 dwb_create_internal — 크기 결정, 포맷, 전역 연결”
// dwb_create_internal -- src/storage/double_write_buffer.cpp
if (!dwb_load_buffer_size (&double_write_buffer_size) || !dwb_load_block_count (&num_blocks))
return NO_ERROR; /* 0 in either param -> DWB disabled */
num_pages = double_write_buffer_size / IO_PAGESIZE;
num_block_pages = num_pages / num_blocks;
assert (IS_POWER_OF_2 (num_blocks));
assert (IS_POWER_OF_2 (num_pages));
assert (IS_POWER_OF_2 (num_block_pages));
assert (num_blocks <= DWB_MAX_BLOCKS); /* block array never overruns scratch */
vdes = fileio_format (thread_p, boot_db_full_name (), dwb_volume_name, LOG_DBDWB_VOLID, num_block_pages, true,
false, false, IO_PAGESIZE, 0, false); /* create+open DWB volume FIRST */
if (vdes == NULL_VOLDES) goto exit_on_error;
fileio_synchronize_all (thread_p); /* flush dirty pages before activating DWB */
error_code = dwb_create_blocks (thread_p, num_blocks, num_block_pages, &blocks);
if (error_code != NO_ERROR) goto exit_on_error;
// ... condensed: dwb_Global.{blocks,num_blocks,num_pages,num_block_pages} = derived geometry ...
dwb_Global.log2_num_block_pages = (unsigned int) (log ((float) num_block_pages) / log ((float) 2));
// ... condensed: counters/cursors = 0; mutex/wait_queue init; vdes; slots_hashmap.init(..., slots_entry_Descriptor) ...
new_position_with_flags = DWB_RESET_POSITION (*current_position_with_flags);
new_position_with_flags = DWB_STARTS_CREATION (new_position_with_flags); /* set CREATE flag */
if (!ATOMIC_CAS_64 (&dwb_Global.position_with_flags, *current_position_with_flags, new_position_with_flags))
assert (false); /* caller holds MODIFY_STRUCTURE; CAS must win */
*current_position_with_flags = new_position_with_flags;
return NO_ERROR;

세 가지 핵심 포인트가 있다. 첫째, 비활성화 게이트: 두 로더 중 하나라도 false를 반환하면 아무것도 건드리지 않고 NO_ERROR를 반환한다. 둘째, 볼륨 우선: LOG_DBDWB_VOLID 볼륨이 포맷되고 이전의 더티 페이지가 sync된 후에야 블록이 생성된다. 셋째, 공개 CAS: 호출자가 modification 플래그를 보유하고 있으므로 assert(false)는 로직 버그가 있을 때만 발동한다. 네 번째 assert는 §3.6 스크래치의 경계를 보장하며, slots_entry_Descriptor는 lock-free 해시의 vtable로 slots hash 활용 지점에서 상세히 다룬다.

Invariant 3-E (CREATE 플래그와 함께 원자적으로 공개되는 지오메트리). 모든 dwb_Global 지오메트리는 CREATE 플래그를 설정하는 단일 ATOMIC_CAS_64 이전에 기록된다. 리더는 DWB_IS_CREATED(position_with_flags)를 확인하며, CAS가 완료되기 전까지는 “미생성” 상태를 보므로 절반만 채워진 dwb_Global을 절대 관찰하지 않는다. CAS가 공개 배리어다.

에러 경로. exit_on_errorvdes가 열려 있으면 볼륨을 dismount하고 unformat하며, blocks가 할당된 경우 0 .. num_blocks-1을 순회하며 dwb_finalize_block을 호출하고 배열을 해제한다. position word는 건드리지 않으므로 전역 상태는 “미생성”으로 남는다.

3.8 dwb_createfileio_make_dwb_name — 공개 래퍼와 이름 생성

섹션 제목: “3.8 dwb_create와 fileio_make_dwb_name — 공개 래퍼와 이름 생성”
// dwb_create -- src/storage/double_write_buffer.cpp
error_code = dwb_starts_structure_modification (thread_p, &current_position_with_flags);
if (error_code != NO_ERROR) return error_code;
if (DWB_IS_CREATED (current_position_with_flags)) goto end; /* already created -> just release */
fileio_make_dwb_name (dwb_Volume_name, dwb_path_p, db_name_p);
error_code = dwb_create_internal (thread_p, dwb_Volume_name, &current_position_with_flags);
if (error_code != NO_ERROR) goto end;
end:
dwb_ends_structure_modification (thread_p, current_position_with_flags); /* always release the flag */
return error_code;

이미 생성됨, 내부 실패, 성공 — 모든 경로가 end:를 통과하므로 modification 플래그는 항상 해제된다(Chapter 10). 이름은 단순한 sprintf다:

// fileio_make_dwb_name -- src/storage/file_io.c
sprintf (dwb_name_p, "%s%s%s%s", dwb_path_p, FILEIO_PATH_SEPARATOR (dwb_path_p), db_name_p, FILEIO_SUFFIX_DWB);

FILEIO_SUFFIX_DWB"_dwb"이므로, /db/log 아래의 demodb/db/log/demodb_dwb가 된다. ID는 고정된 LOG_DBDWB_VOLID = LOG_DBFIRST_VOLID - 22로, 데이터 볼륨이 아닌 예약된 시스템 슬롯이다.

부트 연결. boot_create_database는 첫 번째 데이터 볼륨 이전에 dwb_create를 호출한다:

// boot_create_database -- src/transaction/boot_sr.c
/* DWB creation must be done before first volume. DWB file is created on log_path. */
if (dwb_create (thread_p, log_path, log_prefix) != NO_ERROR)
goto error;
error_code = disk_format_first_volume (thread_p, boot_Db_full_name, db_comments, db_npages);

이 순서는 결정적이다: 첫 번째 데이터 볼륨이 생기기 전에 DWB가 존재해야, 최초의 더티 페이지 플러시부터 torn-page 보호가 적용된다.

3.9 dwb_destroy_internal — 전역 해제 대응 함수

섹션 제목: “3.9 dwb_destroy_internal — 전역 해제 대응 함수”
// dwb_destroy_internal -- src/storage/double_write_buffer.cpp
dwb_destroy_wait_queue (&dwb_Global.wait_queue, &dwb_Global.mutex);
pthread_mutex_destroy (&dwb_Global.mutex);
if (dwb_Global.blocks != NULL) {
for (block_no = 0; block_no < DWB_NUM_TOTAL_BLOCKS; block_no++)
dwb_finalize_block (&dwb_Global.blocks[block_no]); /* mirror of create_blocks pass 4 */
free_and_init (dwb_Global.blocks);
}
dwb_Global.slots_hashmap.destroy ();
if (dwb_Global.vdes != NULL_VOLDES) {
fileio_dismount (thread_p, dwb_Global.vdes); dwb_Global.vdes = NULL_VOLDES;
fileio_unformat (thread_p, dwb_Volume_name); /* delete the on-disk volume */
}
new_position_with_flags = DWB_RESET_POSITION (*current_position_with_flags);
new_position_with_flags = DWB_ENDS_CREATION (new_position_with_flags); /* clear CREATE flag */
if (!ATOMIC_CAS_64 (&dwb_Global.position_with_flags, *current_position_with_flags, new_position_with_flags))
assert (false); /* under MODIFY_STRUCTURE; cannot lose */
*current_position_with_flags = new_position_with_flags;

§3.7의 역순이다: 전역 wait queue와 mutex, 이어서 DWB_NUM_TOTAL_BLOCKS를 순회하며 dwb_finalize_block으로 모든 블록 해제, hashmap 파괴, 볼륨 dismount 및 unformat으로 파일 삭제, 마지막으로 modification 플래그 하에 CREATE 플래그를 해제하는 CAS(Inv 3-E의 역방향). 반환 후 전역 상태는 “미생성”이 되고 _dwb 파일도 사라진다.

  1. dwb_power2_ceil은 네 가지 분기(하한 미만, 상한 초과, 올림, 그대로)로 각 튜너블을 2의 거듭제곱으로 클램프하여 지오메트리가 정확히 나누어지도록 보장한다 — Inv 3-A.
  2. 두 로더 중 하나라도 0이면 단락 평가 ||에 의해 DWB가 원자적으로 비활성화된다 — Inv 3-B.
  3. dwb_create_blocks는 각 slots[i][j].io_page를 하나의 연속된 write_buffer에 앨리어싱하여 벌크 쓰기와 페이지 단위 주소 지정을 동시에 가능하게 하며, dwb_finalize_block은 버퍼를 한 번에 해제한다 — Inv 3-D.
  4. flush_volumes_infonum_block_pages 크기로 확보되어 경계 검사 없는 append가 절대 넘치지 않으며, ensure_metadata는 memset으로 0이 된 후 stage/flush 시점까지 유지된다 — Inv 3-C.
  5. dwb_create_internalLOG_DBDWB_VOLID 볼륨을 포맷하고 이전 더티 페이지를 sync한 뒤 블록을 구성하며, 하나의 ATOMIC_CAS_64로 지오메트리를 공개한다 — Inv 3-E.
  6. fileio_make_dwb_name<log_path>/<db>_dwb를 조합하며, boot_create_databasedisk_format_first_volume 이전에 dwb_create를 호출한다.
  7. dwb_destroy_internal은 §3.7을 역순으로 실행하고 CAS로 CREATE 플래그를 해제하여 “미생성” 상태를 복원한다.

이 챕터는 프로듀서 측의 핵심 질문 하나에 답한다: 스레드는 어떻게 락 없이 다음 빈 DWB 슬롯을 선점하고, 페이지 바이트를 복사해 넣으며, 경쟁과 라이프사이클의 각 분기에서 무엇을 하는가? 락-프리 슬롯 선점은 DWB 쓰기 경로의 핵심이다 — 이 설계인지는 동반 문서의 “Lock-free position cursor” 절을 참고하고, 이 챕터에서는 분기별로 어떻게 동작하는지를 추적한다.

호출 체인은 dwb_set_data_on_next_slot(래퍼) → dwb_acquire_next_slot(락-프리 CAS 선점, dwb_wait_for_strucure_modification 또는 dwb_wait_for_block_completion에서 대기 가능) → dwb_set_slot_data(페이지를 memcpy로 복사)이다. position-with-flags 워드와 매크로는 챕터 2에서 다룬다.

4.1 슬롯 구조체: 선점이 채우는 것

섹션 제목: “4.1 슬롯 구조체: 선점이 채우는 것”

획득 경로는 프로듀서에게 double_write_slot(DWB_SLOT) 하나를 건네준다. §4.2–§4.5가 여섯 개 필드를 읽거나 쓰므로, 먼저 구조체를 파악한다.

// double_write_slot -- src/storage/double_write_buffer.hpp
struct double_write_slot
{
FILEIO_PAGE *io_page; /* The contained page or NULL. */
VPID vpid; /* The page identifier. */
LOG_LSA lsa; /* The page LSA */
bool ensure_metadata; /* Include metadata when syncing */
unsigned int position_in_block;/* The position in block. */
unsigned int block_no; /* The number of the block where the slot reside. */
};
필드역할존재 이유 / 설정 주체
io_page블록 스테이징 버퍼 내 고정 포인터 — 페이지 복사본의 목적지.블록 할당 시 고정 오프셋으로 연결되며(챕터 3), 재할당되지 않는다. dwb_set_slot_data가 프로듀서 페이지를 이 포인터가 가리키는 바이트로 memcpy한다.
vpid스테이징된 페이지의 식별자이자 슬롯의 가시성 플래그.선점 시 NULL로 초기화되고, dwb_set_slot_data마지막으로 재설정한다(Invariant 4-C). 읽기 측은 non-NULL vpid를 유일한 신호로 삼아 슬롯에 완전한 이미지가 있다고 판단한다.
lsa스테이징 시점에 캡처한 페이지의 로그 시퀀스 주소.플러시/복구(챕터 9)가 페이지 헤더를 다시 파싱하지 않고도 LSA로 슬롯을 정렬하거나 조회할 수 있게 한다. io_page_p->prv.lsa에서 복사된다.
ensure_metadata최종 sync 시 볼륨 메타데이터도 함께 플러시할지 여부.프로듀서의 인자를 그대로 전달한다(챕터 6–7에서 이를 처리). dwb_set_slot_data에서 설정된다.
position_in_block블록 내 슬롯의 고정 인덱스, 범위 0 .. DWB_BLOCK_NUM_PAGES-1.할당 시 고정되며 불변이다. 선점 시 커서에서 파생된 오프셋과 일치하는지 assert로 확인한다(Invariant 4-D) — 슬롯↔DWB 볼륨 오프셋 바인딩은 쓰기마다 재계산하지 않는다.
block_no슬롯이 속한 블록 번호.할당 시 고정된다. 슬롯 포인터만 있어도 후속 단계가 소유 블록을 역추적할 수 있게 한다.

획득 경로는 vpid만 NULL로 초기화하고, 스테이징은 io_page내용lsa, vpid, ensure_metadata를 채운다. position_in_blockblock_no는 읽기 전용이다.

유일한 공개 프로듀서 진입점으로, 순수한 순서 제어자다:

// dwb_set_data_on_next_slot -- src/storage/double_write_buffer.cpp
error_code = dwb_acquire_next_slot (thread_p, can_wait, p_dwb_slot); /* <- claim a slot, lock-free */
if (error_code != NO_ERROR) return error_code; /* <- ER_DWB_DISABLED / ER_INTERRUPTED / alloc */
assert (can_wait == false || *p_dwb_slot != NULL); /* <- waiters MUST come back with a slot */
if (*p_dwb_slot == NULL) return NO_ERROR; /* <- non-waiter couldn't claim: not an error */
dwb_set_slot_data (thread_p, *p_dwb_slot, io_page_p, ensure_metadata); /* <- stage the bytes */
return NO_ERROR;

error_code != NO_ERROR는 그대로 전파된다. 성공 후 *p_dwb_slot == NULL획득 불가, 하지만 오류는 아닌 결과이며, can_wait == false인 비-대기자에게만 허용된다.

Invariant 4-A — 대기자는 반드시 슬롯을 들고 돌아온다. can_wait == true일 때 이 함수는 non-NULL *p_dwb_slot을 반환하거나 하드 에러를 반환한다. dwb_acquire_next_slot은 모든 대기 후 goto start로 루프를 돌아 이를 보장한다. NULL-with-NO_ERROR 출구는 can_wait == false 분기에만 존재한다. assert는 대기자가 NULL을 반환하는 경우를 잡아낸다 — release 빌드에서 그 경우는 보장된 페이지 쓰기를 조용히 누락시킨다.

4.3 dwb_acquire_next_slot: 락-프리 선점

섹션 제목: “4.3 dwb_acquire_next_slot: 락-프리 선점”

start: 레이블의 read-modify-CAS 재시도 루프로, 라이프사이클/경쟁 프리앰블을 감싼다. 함수는 먼저 출력 파라미터를 NULL로 초기화(*p_dwb_slot = NULL)하므로, 조기 return NO_ERROR 분기는 모두 호출자에게 NULL을 남긴다. Figure 4-1은 전체 분기 지도이며, 이하 산문은 각 분기를 순서대로 설명한다.

flowchart TD
  A["start: 커서 원자적 읽기"] --> B{NOT_CREATED_OR_MODIFYING?}
  B -- "no, 정상" --> P{position_in_block == 0?}
  B -- "yes, 드문 경우" --> C{MODIFYING_STRUCTURE?}
  C -- "yes" --> C1{can_wait?}
  C1 -- "false" --> CR["return NO_ERROR, slot NULL"]
  C1 -- "true" --> C2["wait_for_strucure_modification"]
  C2 --> C3{rc?}
  C3 -- "TIMEDOUT 또는 NO_ERROR" --> A
  C3 -- "기타 에러" --> CE["return error_code"]
  C -- "no" --> D{!IS_CREATED?}
  D -- "yes" --> D1{ANY_BLOCK_WRITE_STARTED?}
  D1 -- "yes" --> DE["ER_DWB_DISABLED"]
  D1 -- "no" --> DR["return NO_ERROR, slot NULL"]
  D -- "no, 불가능" --> DX["assert false"]
  P -- "yes, 첫 번째 writer" --> E{BLOCK_WRITE_STARTED?}
  E -- "yes" --> E1{can_wait?}
  E1 -- "false" --> ER["return NO_ERROR, slot NULL"]
  E1 -- "true" --> E2["wait_for_block_completion"]
  E2 --> E3{rc?}
  E3 -- "TIMEDOUT 또는 NO_ERROR" --> A
  E3 -- "기타 에러" --> EE["return error_code"]
  E -- "no" --> F["new = NEXT(STARTS_BLOCK_WRITING)"]
  P -- "no, 블록 중간" --> G["new = NEXT(pos)"]
  F --> H{ATOMIC_CAS_64 성공?}
  G --> H
  H -- "no, 경쟁 패배" --> A
  H -- "yes" --> I["슬롯 선점, vpid NULL, return"]

Figure 4-1. dwb_acquire_next_slot의 전체 분기.

매 루프 패스마다 ATOMIC_INC_64 (&dwb_Global.position_with_flags, 0ULL)start: 레이블에서 커서를 다시 읽는다 — 0을 더하는 것은 원자적 로드의 관용구다. 매번 재읽기하기 때문에 루프는 항상 정확하다: CAS 경쟁에 진 스레드, 대기에서 깨어난 스레드, 또는 과도적 플래그를 본 스레드 모두 goto start 후에는 최신 커서 상태를 반영한다.

4.3.1 드문 라이프사이클 프리앰블: DWB_NOT_CREATED_OR_MODIFYING

섹션 제목: “4.3.1 드문 라이프사이클 프리앰블: DWB_NOT_CREATED_OR_MODIFYING”

커서가 “생성됨, 수정 중 아님” 상태가 아닌 경우에 해당하며, 세 갈래로 분기된다.

// dwb_acquire_next_slot -- src/storage/double_write_buffer.cpp
if (DWB_IS_MODIFYING_STRUCTURE (current_position_with_flags)) /* <- (a) resize/recreate underway */
{
if (can_wait == false) return NO_ERROR; /* <- non-waiter bails, slot NULL */
error_code = dwb_wait_for_strucure_modification (thread_p);
if (error_code != NO_ERROR)
{
if (error_code == ER_CSS_PTHREAD_COND_TIMEDOUT) goto start; /* <- timeout not failure: retry */
return error_code; /* <- alloc fail / ER_INTERRUPTED */
}
goto start; /* <- clean wake: re-read cursor */
}
else if (!DWB_IS_CREATED (current_position_with_flags)) /* <- (b) buffer gone */
{
if (DWB_IS_ANY_BLOCK_WRITE_STARTED (current_position_with_flags))
{ er_set (..., ER_DWB_DISABLED, 0); return ER_DWB_DISABLED; } /* <- partial teardown: hard error */
return NO_ERROR; /* <- fully off, nothing in flight */
}
else
assert (false); /* <- (c) unreachable debug guard */

(a) 구조 수정 중. ER_CSS_PTHREAD_COND_TIMEDOUT은 전파되지 않는다 — goto start로 재시도된다. 그 외 에러(할당 실패, 셧다운 시 ER_INTERRUPTED)는 반환한다. 정상 wake-up도 goto start하는데, 수정이 끝났다는 의미일 뿐이므로 커서를 다시 읽어야 하기 때문이다.

(b) 생성되지 않은 상태. 아직 쓰기 시작 비트가 설정된 블록이 있다면, 이는 비정상적인 부분 해제 상태이므로 ER_DWB_DISABLED를 반환한다. 그렇지 않으면 DWB가 단순히 꺼진 것이며, 호출자는 페이지를 직접 쓴다.

(c) 도달 불가 분기. DWB_NOT_CREATED_OR_MODIFYING(pos & DWB_CREATE_OR_MODIFY_MASK) != DWB_CREATE로 정의된다. 두 if 분기가 이 조건의 모든 경우를 소진하므로 else는 도달 불가다. assert 뒤에 return이 없다는 점에 주의 — release 빌드에서는 비정상 커서 상태로 정상 경로로 fall-through하므로, 이것은 순수한 디버그 가드다.

4.3.2 커서 디코딩과 블록 첫 진입 분기

섹션 제목: “4.3.2 커서 디코딩과 블록 첫 진입 분기”

프리앰블을 통과하면, 커서를 블록 번호(DWB_GET_BLOCK_NO_FROM_POSITION)와 블록 내 오프셋(DWB_GET_POSITION_IN_BLOCK)으로 분리하고, 각각 DWB_NUM_TOTAL_BLOCKS / DWB_BLOCK_NUM_PAGES를 기준으로 범위 assert를 수행한다. 판단의 핵심은 현재 스레드가 블록에 첫 번째 프로듀서인지 여부다.

// dwb_acquire_next_slot -- src/storage/double_write_buffer.cpp
if (position_in_current_block == 0)
{
if (DWB_IS_BLOCK_WRITE_STARTED (current_position_with_flags, current_block_no))
{
if (can_wait == false) return NO_ERROR; /* <- non-waiter bails */
error_code = dwb_wait_for_block_completion (thread_p, current_block_no); /* <- prev fill unflushed */
if (error_code != NO_ERROR)
{
if (error_code == ER_CSS_PTHREAD_COND_TIMEDOUT) goto start; /* <- timeout: retry */
return error_code;
}
goto start; /* <- block flushed and reset: re-read */
}
assert (!DWB_IS_BLOCK_WRITE_STARTED (current_position_with_flags, current_block_no)); /* <- bit clear */
current_position_with_block_write_started =
DWB_STARTS_BLOCK_WRITING (current_position_with_flags, current_block_no); /* <- set block's bit */
new_position_with_flags = DWB_GET_NEXT_POSITION_WITH_FLAGS (current_position_with_block_write_started);
}

첫 번째 프로듀서는 해당 비트가 클리어임을 확인한 후에만 쓰기 시작 비트를 설정한다. 비트가 여전히 set이라면 이전 채우기 작업이 아직 플러시되지 않은 것이고, 그 슬롯을 재사용하면 플러시가 아직 필요로 하는 데이터를 덮어쓰게 된다(래핑 해저드 — 단일 블록 버퍼에서는 드물지 않다). can_wait == false이면 바로 bail-out하고, 대기자는 dwb_wait_for_block_completion(§4.4)을 호출하고 정상 wake-up 시 goto start로 돌아간다. 비트가 클리어이면, DWB_STARTS_BLOCK_WRITING이 상태 비트를 OR로 설정하고 DWB_GET_NEXT_POSITION_WITH_FLAGS가 위치를 전진시킨다 — 두 작업이 하나의 64비트 워드에서 이루어지므로, CAS가 둘을 원자적으로 공표한다.

position_in_current_block != 0 — 블록 중간 writer. else 분기는 두 가지 디버그 assert(DWB_IS_CREATED(pos)!DWB_IS_MODIFYING_STRUCTURE(pos)) 아래에서 위치만 전진시킨다(new = DWB_GET_NEXT_POSITION_WITH_FLAGS(pos)).

Invariant 4-B — 첫 번째가 아닌 writer는 해제 경쟁을 피할 수 있다. 어떤 스레드가 슬롯 0을 선점한 순간, 해당 블록의 쓰기 시작 비트는 그 블록이 플러시될 때까지 DWB를 “생성됨, 수정 중 아님” 상태로 고정한다. position_in_current_block != 0을 보는 스레드는 두 플래그가 모두 유지됨을 전제할 수 있으며, assert가 디버그 빌드에서 이를 강제한다. 위반 시 프로듀서는 해제 중인 블록에 스테이징할 수 있다.

// dwb_acquire_next_slot -- src/storage/double_write_buffer.cpp
if (!ATOMIC_CAS_64 (&dwb_Global.position_with_flags, current_position_with_flags, new_position_with_flags))
goto start; /* <- lost the race: cursor moved, retry */
block = dwb_Global.blocks + current_block_no;
*p_dwb_slot = block->slots + position_in_current_block; /* <- claimed slot: block base + offset */
VPID_SET_NULL (& (*p_dwb_slot)->vpid); /* <- invalidate slot content */
assert ((*p_dwb_slot)->position_in_block == position_in_current_block);
return NO_ERROR;

CAS는 선형화 지점이다: 읽기 이후 커서가 변경되지 않은 경우에만 성공한다. 실패 시 스레드는 새로운 읽기부터 goto start로 재시도한다(CAS 경쟁에 진 첫 번째 writer는 시작 비트를 다시 설정하지 않는다). 성공 시 슬롯을 독점적으로 소유하고(동일 커서 값에서 CAS 성공자는 둘이 될 수 없다) vpid를 NULL로 초기화한다.

Invariant 4-D — 슬롯 인덱스는 DWB 볼륨 오프셋에 1:1 대응한다. 선점된 슬롯은 blocks[block_no].slots + position_in_block이며, slots[i].position_in_block == i는 블록 빌드 시 고정된다(챕터 3). 마지막 assert가 이를 커서 파생 오프셋과 대조 확인한다. 이 인덱스는 DWB 볼륨에서 슬롯의 고정 오프셋이기도 하므로, 커서 위치 p에서 획득한 슬롯은 항상 동일한 온-디스크 위치에 쓴다 — 목적지를 프로듀서가 고르는 것이 아니라 커서가 결정한다.

두 대기 헬퍼는 서버 모드 전용이다(!SERVER_MODE에서는 NO_ERROR를 반환하는 no-op). 구조는 같다: mutex를 잠그고, mutex 하에서 재검사하고, 큐에 등록하고, 타임아웃을 두어 suspend하고, 재개 결과를 분류한다.

dwb_wait_for_strucure_modification (원본 코드에서 “structure”를 오타로 표기 — 실제 심볼 이름임)은 10 ms 타임아웃으로 dwb_Global.mutex 하에서 전역 dwb_Global.wait_queue에서 대기한다:

// dwb_wait_for_strucure_modification -- src/storage/double_write_buffer.cpp
(void) pthread_mutex_lock (&dwb_Global.mutex);
current_position_with_flags = ATOMIC_INC_64 (&dwb_Global.position_with_flags, 0ULL);
if (!DWB_IS_MODIFYING_STRUCTURE (current_position_with_flags))
{ pthread_mutex_unlock (&dwb_Global.mutex); return NO_ERROR; } /* <- test, lock, re-test: don't sleep */
// ... condensed: thread_lock_entry; enqueue on dwb_Global.wait_queue (NULL -> error); unlock ...
r = thread_suspend_timeout_wakeup_and_unlock_entry (thread_p, &to, THREAD_DWB_QUEUE_SUSPENDED); /* <- 10 ms */

mutex 하에서의 재검사는 이미 클리어된 조건에서 sleep하는 것을 막는다. 재개 분기는 셋: ER_CSS_PTHREAD_COND_TIMEDOUT → 엔트리 제거 후 타임아웃 반환(호출자 재시도); resume_status != THREAD_DWB_QUEUE_RESUMED → 셧다운 wake, ER_INTERRUPTED; 그 외 → dwb_signal_structure_modificated로 깨어남, NO_ERROR.

**dwb_wait_for_block_completion**은 구조는 동일하나, 블록별 dwb_block->wait_queue에서 dwb_block->mutex 하에 대기하고, DWB_IS_BLOCK_WRITE_STARTED로 블록의 시작 비트를 재검사하며, 20 ms 타임아웃을 사용한다. 두 변형이 다른 점은 per-thread lock 획득 순서와 재검사 시점이다: block-completion은 dwb_block->mutex를 잠근 뒤 thread_lock_entry를 하고 그 다음 재검사한다(두 lock을 모두 쥔 채 재검사); structure-mod는 dwb_Global.mutex를 먼저 잠그고 DWB_IS_MODIFYING_STRUCTURE를 재검사한 다음 큐 등록을 결정할 때만 thread_lock_entry를 호출한다. 어느 쪽이든 mutex 하 재검사가 이미 클리어된 조건에서의 sleep을 방지한다. TIMEDOUT 분기도 다르다: block-completion은 dwb_remove_wait_queue_entrydwb_set_status_resumed를 전달하고, structure-mod 변형은 모든 분기에서 NULL을 전달한다. 큐 메커니즘과 dwb_signal_* 웨이커는 챕터 10에서 다룬다. 계약: 모든 대기는 NO_ERROR(재시도), ER_CSS_PTHREAD_COND_TIMEDOUT(재시도), 또는 하드 에러(전파) 중 하나를 반환한다. goto start는 모든 재시도에서 커서를 다시 읽는다.

4.5 바이트 스테이징: dwb_set_slot_data

섹션 제목: “4.5 바이트 스테이징: dwb_set_slot_data”

슬롯을 소유한 상태에서 dwb_set_slot_data는 페이지를 복사한다. 커서를 건드리지 않으므로 원자 연산이 필요 없다 — CAS가 이미 소유권을 부여했기 때문이다.

// dwb_set_slot_data -- src/storage/double_write_buffer.cpp
assert (io_page_p->prv.p_reserve_2 == 0);
if (io_page_p->prv.pageid != NULL_PAGEID)
memcpy (dwb_slot->io_page, (char *) io_page_p, IO_PAGESIZE); /* <- real page: copy verbatim */
else
fileio_initialize_res (thread_p, dwb_slot->io_page, IO_PAGESIZE); /* <- "hole" page: clean reserved page */
assert (fileio_is_page_sane (io_page_p, IO_PAGESIZE));
LSA_COPY (&dwb_slot->lsa, &io_page_p->prv.lsa); /* <- capture LSA */
VPID_SET (&dwb_slot->vpid, io_page_p->prv.volid, io_page_p->prv.pageid);/* <- set LAST: now identifiable */
dwb_slot->ensure_metadata = ensure_metadata;

분기는 prv.pageid에 따른다: 실제 페이지(!= NULL_PAGEID)는 슬롯의 io_pagememcpy 전체 복사되고, hole(== NULL_PAGEID)은 fileio_initialize_res로 정형화된 이미지를 만든다. 그 다음 lsa, vpid(반쯤 스테이징된 슬롯이 아직 NULL-VPID로 읽히도록 마지막에 설정), ensure_metadata 순으로 채운다. assert는 무결성을 검사한다: 헤더 예약 필드 prv.p_reserve_2 == 0, 소스 페이지(복사본이 아님)에 대한 fileio_is_page_sane.

Invariant 4-C — VPID는 슬롯의 가시성 플래그다. 슬롯은 vpid가 non-NULL일 때만 읽기 측에 유효하다. dwb_acquire_next_slot이 선점 시 NULL로 초기화하고, dwb_set_slot_data는 바이트와 LSA가 모두 설정된 후에야 vpid를 재설정한다. 따라서 non-NULL VPID를 발견한 읽기 측(챕터 8)은 완전히 스테이징된 이미지와 LSA를 보장받는다. 쓰기 순서만으로 이를 보장하며, 슬롯별 락은 없다.

  1. 선점은 DWB_SLOT 하나를 반환하며, 획득 시점에는 vpid만 건드린다. io_page, position_in_block, block_no는 챕터 3에서 고정된 불변 값이다. 스테이징이 이후에 io_page의 내용, lsa, vpid, ensure_metadata를 채운다.
  2. dwb_acquire_next_slotstart: 레이블의 read-modify-CAS 루프다ATOMIC_INC_64(&pos, 0)로 읽고, ATOMIC_CAS_64로 커밋하며, 경쟁 패배/대기/플래그 변경은 모두 goto start로 귀결된다.
  3. 라이프사이클 프리앰블은 네 분기이며 하나만 정상 진행한다: 구조 수정 중(대기 또는 bail-out), !DWB_IS_CREATEDER_DWB_DISABLED(부분 해제) vs. NULL-NO_ERROR(정상 비활성), 도달 불가 assert(false).
  4. 첫 번째 writer는 시작 비트 설정과 위치 전진을 하나의 CAS 워드로 처리한다. 비트가 이미 설정돼 있으면 이전 채우기가 미플러시된 것이므로 래핑을 피하기 위해 dwb_wait_for_block_completion으로 대기한다.
  5. can_wait이 실패 모드를 결정한다: false이면 NULL-NO_ERROR 반환(Invariant 4-A), true이면 블록하고 ER_CSS_PTHREAD_COND_TIMEDOUT은 항상 재시도다.
  6. 선점된 슬롯 인덱스는 DWB 볼륨 오프셋에 고정 대응한다(Invariant 4-D)blocks[block_no].slots[position_in_block], 선점 시 assert로 검증하며, 프로듀서가 목적지를 선택하지 않는다.
  7. dwb_set_slot_data는 원자 연산이 불필요하다 (CAS가 소유권을 부여했다). prv.pageid == NULL_PAGEID(reserved) vs. 실제 페이지(memcpy)로 분기하고, LSA, VPID, ensure_metadata를 순서대로 설정한다.
  8. VPID는 슬롯의 가시성 플래그다(Invariant 4-C) — 선점 시 NULL로 초기화하고 마지막에 재설정한다. 쓰기 순서만으로 반쯤 스테이징된 슬롯을 감추며, 슬롯별 락은 없다. 스테이징이 끝나면 슬롯은 독자가 발견할 수 있지만, 해시 인덱싱이나 플러시 카운팅은 되지 않는다 — 그 승격은 챕터 5에서 다룬다.

Chapter 4에서 우리는 staged 상태의 슬롯을 얻었다. (block_no, position_in_block) 예약 위치에 페이지 이미지와 vpid/lsa를 담고 있지만, 동시 실행 중인 다른 스레드에게는 아직 보이지 않는 상태다. 이 챕터는 두 가지 질문에 답한다. staged 슬롯은 어떻게 발견 가능한 상태가 되는가, 그리고 블록의 마지막 슬롯이 채워졌을 때 flush는 어떻게 시작되는가? 두 가지 모두 dwb_add_page 안에서 일어난다. (1) dwb_slots_hash_insert로 슬롯을 VPID 해시에 공개하고, (2) 블록별 채움 카운터를 증가시켜 블록이 가득 찼는지 여부에 따라 분기한다.

CUBRID가 진행 중인 페이지(in-flight page)의 VPID 인덱스를 유지하는 이유는 상위 문서 cubrid-double-write-buffer.md의 “Why a hash over the staged pages” / “Decache after write” 절을 참고하라. 여기서는 재유도하지 않는다.

5.1 STAGED에서 HASHED로의 전환과 불변 조건

섹션 제목: “5.1 STAGED에서 HASHED로의 전환과 불변 조건”

슬롯의 상태 전이는 FREE -> STAGED (Chapter 4) -> {HASHED | INVALIDATED} -> FLUSHED (Chapter 6) 순서를 따른다. 이 챕터는 STAGED 이후의 화살표를 다룬다. dwb_slots_hash_insertinserted == true를 반환하면 STAGED -> HASHED 전환이 일어나고, 경쟁에서 지면 STAGED -> INVALIDATED가 되며 해당 슬롯은 나중에 0으로 채워진 무효 페이지로 기록된다.

표준 무효화 원시 연산(canonical invalidation primitive). 이후 절에서 “슬롯을 무효화한다”고 할 때는 항상 두 가지 쓰기를 의미한다. VPID_SET_NULL (&slot->vpid)로 VPID를 null로 만들어 dwb_slots_hash_delete가 이 슬롯을 건너뛰게 하고(null VPID에서 즉시 반환, §5.5), fileio_initialize_res (thread_p, slot->io_page, IO_PAGESIZE)로 이미지를 0으로 초기화한다. 슬롯은 여전히 자신의 위치를 유지하며 DWB 볼륨에 기록되지만, 유효한 데이터를 담지 않으며 해시 엔트리도 소유하지 않는다. 둘 중 하나라도 생략하면 유령 해시 삭제나 오래된 데이터의 복구 재실행(recovery replay) 위험이 생긴다(Chapter 9).

불변 조건 (STAGED-before-HASHED 가시성). 읽기 측이 슬롯을 발견할 수 있는 것은 오직 dwb_slots_hash_insertslots_hash_entry->slot = slot을 저장한 이후다. 코드는 이 포인터를 엔트리 mutex 아래에서, 모든 LSA/블록 결정이 끝난 뒤 마지막 쓰기로 공개한다. 순서가 바뀐다면 dwb_read_page의 읽기 측이 절반만 기록된 슬롯을 역참조해 잘린 페이지를 돌려줄 수 있다. slots_hash_entry mutex는 같은 VPID에 대한 삽입/삭제 간의 교환을 원자적으로 만든다.

5.2 dwb_add_page — 슬롯 재획득 폴백과 공개 단계

섹션 제목: “5.2 dwb_add_page — 슬롯 재획득 폴백과 공개 단계”

dwb_add_page는 이미 staged된 슬롯을 받거나(*p_dwb_slot != NULL), 슬롯 없이 호출(*p_dwb_slot == NULL)된다. 후자의 경우 첫 번째 분기에서 처리한다.

// dwb_add_page -- src/storage/double_write_buffer.cpp
assert (p_dwb_slot != NULL && (io_page_p != NULL || (*p_dwb_slot)->io_page != NULL) && vpid != NULL);
// ... condensed: resolve thread_p if NULL ...
if (*p_dwb_slot == NULL)
{
error_code = dwb_set_data_on_next_slot (thread_p, io_page_p, true, ensure_metadata, p_dwb_slot); /* can_wait = true */
if (error_code != NO_ERROR)
return error_code;
if (*p_dwb_slot == NULL)
return NO_ERROR; /* no DWB / no slot; nothing to publish */
}
dwb_slot = *p_dwb_slot;
assert (VPID_EQ (vpid, &dwb_slot->vpid)); /* staged vpid matches caller's */

네 가지 경우가 있다. 이미 staged된 슬롯은 그대로 사용하고, dwb_set_data_on_next_slot 오류는 상위로 전파한다. can_wait = true임에도 재획득 결과가 NULL이면 (DWB 없음 또는 슬롯 없음) NO_ERROR를 반환해 호출자가 페이지를 직접 기록하도록 한다. 정상적으로 슬롯을 재획득한 경우에는 이후 처리로 넘어간다. assert (VPID_EQ ...) 는 호출자의 vpid와 staging 시 dwb_set_slot_data가 기록한 dwb_slot->vpid가 일치하는지 검증한다. 불일치하면 호출자가 한 페이지를 staging하고 다른 페이지를 공개하는 것이다. io_page->prv와의 삼각 검증은 dwb_slots_hash_insert 안에서 수행된다(§5.4).

공개 단계:

// dwb_add_page -- src/storage/double_write_buffer.cpp
if (!VPID_ISNULL (vpid))
{
error_code = dwb_slots_hash_insert (thread_p, vpid, dwb_slot, &inserted);
if (error_code != NO_ERROR)
return error_code;
if (!inserted)
{
/* lost the race: invalidate dwb_slot per 5.1 */
VPID_SET_NULL (&dwb_slot->vpid);
fileio_initialize_res (thread_p, dwb_slot->io_page, IO_PAGESIZE);
}
}

분기별 설명: null VPID(예: 메타데이터 전용 flush 마커)는 삽입을 건너뛰지만 count_wb_pages에는 계속 반영된다. 삽입 오류는 전파된다. inserted == true이면 이 슬롯이 해당 VPID의 소유자가 된다. inserted == false(!inserted 경로)이면 더 나은 또는 동등한 슬롯이 이미 이 VPID를 소유하고 있다는 의미이므로(§5.4), §5.1에 따라 이 슬롯을 무효화한다.

삽입 여부와 관계없이 모든 호출은 해당 슬롯을 블록에 반영한다. 카운터는 DWB_BLOCK 구조체의 volatile unsigned int count_wb_pages 필드이며, 원자 연산이 직접 수정한다.

// dwb_add_page -- src/storage/double_write_buffer.cpp
// ... condensed: dwb_log ("added page ... on block ... position ...") ...
block = &dwb_Global.blocks[dwb_slot->block_no];
count_wb_pages = ATOMIC_INC_32 (&block->count_wb_pages, 1);
assert_release (count_wb_pages <= DWB_BLOCK_NUM_PAGES); /* never overfill */
if (count_wb_pages < DWB_BLOCK_NUM_PAGES)
needs_flush = false;
else
needs_flush = true;
if (needs_flush == false)
return NO_ERROR;

ATOMIC_INC_32는 “마지막 슬롯을 쓴 스레드” 결정을 직렬화하므로, 정확히 하나의 호출자만 count_wb_pages == DWB_BLOCK_NUM_PAGES를 관찰하고 flush 분기를 탄다. assert_release(프로덕션에서도 제거되지 않음)는 상한을 보호한다. 이것이 발동하면 Chapter 4에서 위치를 과하게 배분했다는 의미다. 무효화된 !inserted 슬롯도 실제 위치를 차지하므로 카운터를 증가시킨다. needs_flush == false인 일반적인 경우는 즉시 반환한다.

블록이 가득 찼을 때의 flush 분기:

// dwb_add_page -- src/storage/double_write_buffer.cpp
#if defined (SERVER_MODE)
if (dwb_is_flush_block_daemon_available ())
{
dwb_flush_block_daemon->wakeup (); /* <- hand off, do not block the writer */
return NO_ERROR;
}
#endif /* SERVER_MODE */
error_code = dwb_flush_block (thread_p, block, false, NULL); /* <- inline flush */
if (error_code != NO_ERROR)
{
dwb_log_error (...); /* <- "Can't flush block ..."; then propagate */
return error_code;
}

SERVER_MODE에서 daemon이 활성화된 경우 daemon을 깨우고 즉시 반환한다. flush 지연이 없고 이전 블록을 기다리지도 않는다(순서 보장은 daemon의 역할, Chapter 7). Standalone이거나 daemon이 없는 경우 호출자가 dwb_flush_block (thread_p, block, false, NULL)로 인라인 flush를 수행한다. 첫 번째 falsefile_sync_helper_can_flush 파라미터이고, NULLcurrent_position_with_flags다(프로토타입 확인 완료). dwb_flush_block 자체가 dwb_write_block에 전달할 remove_from_hash 값을 결정하며, 이 함수가 해시 삭제 루프를 실행한다(§5.5). Chapter 6에서 자세히 다룬다.

5.4 dwb_slots_hash_insert — find-or-insert와 LSA 분기

섹션 제목: “5.4 dwb_slots_hash_insert — find-or-insert와 LSA 분기”

이 함수는 *vpid에 대한 엔트리를 find-or-insert한 뒤, 슬롯이 소유권을 획득할지 결정한다.

// dwb_slots_hash_insert -- src/storage/double_write_buffer.cpp
*inserted = dwb_Global.slots_hashmap.find_or_insert (thread_p, *vpid, slots_hash_entry);
assert (VPID_EQ (&slots_hash_entry->vpid, &slot->vpid));
assert (slots_hash_entry->vpid.pageid == slot->io_page->prv.pageid
&& slots_hash_entry->vpid.volid == slot->io_page->prv.volid);

find_or_insert는 새 엔트리를 생성했으면 true(VPID 없음), 기존 엔트리가 있었으면 false를 반환한다. 두 경우 모두 mutex를 잡은 채로 slots_hash_entry를 반환하므로 이 함수는 모든 종료 경로에서 반드시 unlock해야 한다. 두 개의 assert는 VPID 삼각 검증을 수행한다. 해시 키, 슬롯의 vpid, io_page->prv가 모두 일치해야 한다.

flowchart TD
  A[find_or_insert, mutex 획득] --> B{inserted == true?}
  B -- 예, 새 엔트리 --> Z[이 슬롯이 소유자]
  B -- 아니오, 기존 엔트리 존재 --> C{LSA_LT?}
  C -- 예, 이 슬롯이 더 오래됨 --> D[unlock, 반환, inserted 유지 false]
  C -- 아니오 --> E{LSA_EQ?}
  E -- 예 --> F{같은 block_no?}
  F -- 예 --> G[기존 슬롯 무효화 per 5.1]
  F -- 아니오 --> H[디버그 빌드 전용: 이전 블록 순서 assert]
  E -- 아니오, 이 슬롯이 더 최신 --> I[무효화 없음]
  G --> J[ensure_metadata OR 병합]
  H --> J
  I --> J
  J --> Z
  Z --> K[슬롯 포인터 교체, unlock, inserted = true]

Figure 5-1: dwb_slots_hash_insert 결정 트리.

신규 삽입 경로(*inserted == true)는 곧바로 포인터 교체로 간다. 충돌 경로(*inserted == false)는 기존 소유자가 있으며, 세 가지 LSA 하위 분기를 거친다(Fig. 5-1).

// dwb_slots_hash_insert -- src/storage/double_write_buffer.cpp
if (! (*inserted))
{
assert (slots_hash_entry->slot != NULL);
if (LSA_LT (&slot->lsa, &slots_hash_entry->slot->lsa))
{ /* existing owner is newer - leave it; *inserted stays false */
pthread_mutex_unlock (&slots_hash_entry->mutex);
return NO_ERROR;
}
else if (LSA_EQ (&slot->lsa, &slots_hash_entry->slot->lsa))
{
if (slots_hash_entry->slot->block_no == slot->block_no)
{
assert (slots_hash_entry->slot->position_in_block < slot->position_in_block);
VPID_SET_NULL (&slots_hash_entry->slot->vpid); /* invalidate OLD slot per 5.1 */
fileio_initialize_res (thread_p, slots_hash_entry->slot->io_page, IO_PAGESIZE);
}
else
{
#if !defined (NDEBUG)
int old_block_no = ATOMIC_INC_32 (&slots_hash_entry->slot->block_no, 0); /* atomic read */
if (old_block_no > 0)
{
DWB_BLOCK *old_block = &dwb_Global.blocks[old_block_no];
DWB_BLOCK *new_block = &dwb_Global.blocks[slot->block_no];
/* assert the OLD page's block flushes first (by version, then block_no) */
assert ((old_block->version < new_block->version)
|| (old_block->version == new_block->version
&& old_block->block_no < new_block->block_no));
}
#endif
}
}
slot->ensure_metadata = slot->ensure_metadata || slots_hash_entry->slot->ensure_metadata; /* OR-merge */
}

LSA가 더 작다는 것은 더 오래된 버전임을 의미한다. 따라서 LSA_LT(이 슬롯이 더 오래됨)는 기존 소유자를 유지하고 inserted == false로 조기 반환한다. §5.2에서 이 슬롯을 무효화한다. **LSA_EQ**는 “로깅 없이 수정됨” 케이스로, 두 슬롯이 정당하게 같은 LSA를 공유한다. 같은 블록의 패배 슬롯은 §5.1에 따라 제자리에서 무효화되며, 기존 소유자를 대상으로 한다. 블록 flush 시 중복 VPID가 없도록 보장하기 위해서다. 다른 블록의 패배 슬롯은 무효화할 필요 없이 독립적으로 flush된다. #if !defined (NDEBUG) 블록은 디버그 빌드에서만 컴파일되며, 이전 소유자의 block_noATOMIC_INC_32 (..., 0)으로 읽고(동시 flush가 0으로 만들었을 수 있음), 그 값이 > 0인 경우에만 이전 페이지의 블록이 새 블록보다 flush 순서가 앞서는지 assert한다: assert ((old_block->version < new_block->version) || (old_block->version == new_block->version && old_block->block_no < new_block->block_no)). 이는 블록 간 크래시 복구 순서를 유지하기 위함이다(Chapter 9). 암묵적 else인 newer-LSA 경로는 무효화가 필요 없다. 기존 소유자는 포인터 교체로 분리되고 dwb_slots_hash_delete가 더 이상 매칭하지 않는다(§5.5). ensure_metadata OR 병합은 모든 충돌 하위 분기에서 실행되어, 교체된 버전에 대한 메타데이터 동기화 요청이 유실되지 않도록 한다.

포인터 교체는 신규 삽입과 LSA_EQ/newer-LSA 경로에서 도달하지만, LSA_LT 조기 반환 경로에서는 실행되지 않는다.

// dwb_slots_hash_insert -- src/storage/double_write_buffer.cpp
slots_hash_entry->slot = slot; /* <- publish: now discoverable */
pthread_mutex_unlock (&slots_hash_entry->mutex);
*inserted = true; /* <- normalize: "won" == inserted */
return NO_ERROR;

이 저장 연산이 STAGED -> HASHED 전환의 실체다(§5.1). mutex 해제 직전의 마지막 쓰기로, 가시성 불변 조건을 만족한다. 마지막 *inserted = true는 “새로 생성”과 “기존 소유자 교체” 두 경우를 모두 정규화한다. 두 경우 모두 이 슬롯이 VPID를 소유하므로 무효화되어서는 안 된다. LSA_LTfalse를 반환한다.

5.5 dwb_slots_hash_delete — 쓰기 이후의 decache

섹션 제목: “5.5 dwb_slots_hash_delete — 쓰기 이후의 decache”

반대 방향 연산으로, 블록의 페이지들이 홈 볼륨에 도달한 뒤 dwb_write_blockif (remove_from_hash) 루프에서 null이 아닌 슬롯마다 호출된다. 이 함수는 슬롯을 제거해 이후 읽기가 페이지 버퍼/홈 볼륨으로 향하게 하고, DWB 복사본을 참조하지 않게 한다.

// dwb_slots_hash_delete -- src/storage/double_write_buffer.cpp
vpid = &slot->vpid;
if (VPID_ISNULL (vpid))
return NO_ERROR; /* invalidated; never hashed */
slots_hash_entry = dwb_Global.slots_hashmap.find (thread_p, *vpid);
if (slots_hash_entry == NULL)
return NO_ERROR; /* already removed */
assert (VPID_ISNULL (&slots_hash_entry->slot->vpid)
|| VPID_EQ (&slots_hash_entry->slot->vpid, vpid));
if (slots_hash_entry->slot == slot) /* only erase if THIS slot owns it */
{
if (!dwb_Global.slots_hashmap.erase_locked (thread_p, *vpid, slots_hash_entry))
{
assert_release (false); /* <- erase under held mutex must succeed */
pthread_mutex_unlock (&slots_hash_entry->mutex);
dwb_log_error ("DWB hash delete key (%d, %d) ...", vpid->volid, vpid->pageid, error_code);
return error_code; /* error_code is still NO_ERROR here */
}
}
else
pthread_mutex_unlock (&slots_hash_entry->mutex); /* <- newer slot owns it; leave it */
return NO_ERROR;

분기별 설명: VPID_ISNULL 슬롯은 §5.1에서 무효화되어 해시에 등록된 적이 없다. VPID를 null로 만드는 것이 정확히 이 경우를 처리하기 위해서다. findNULL을 반환하면 엔트리가 이미 사라진 것이다(no-op; find는 엔트리를 찾을 때 mutex를 잡은 채로 반환한다). slot == slot이면 erase_locked로 삭제한다(mutex를 소비함). mutex를 잡은 상태에서 실패하는 것은 있어서는 안 되는 상황이며, assert_release (false)dwb_log_error로 표시한다. 이때 return error_code는 지역 변수가 재할당된 적 없으므로 여전히 NO_ERROR를 담고 있어, 프로덕션에서도 오류 전파 없이 조용한 no-op으로 처리된다. else 경로는 이 슬롯이 hashing된 이후 더 최신 슬롯이 교체된 경우다. 이 오래된 슬롯이 엔트리를 삭제하면 현재 소유자를 파괴하므로 삭제해서는 안 된다.

불변 조건 (현재 소유자만 decache). 슬롯은 slots_hash_entry->slot == slot인 경우에만 엔트리를 삭제한다. §5.4의 포인터 교체 덕분에 VPID 엔트리는 매 순간 하나의 명확한 소유자를 갖는다.

해시에 저장되는 객체는 DWB_SLOTS_HASH_ENTRY다. 페이로드(vpid, slot 포인터, mutex) 외에, slots_entry_Descriptor(LF_ENTRY_DESCRIPTOR)에 등록된 lockfree-hashmap 배관 필드 세 개가 있다.

필드역할
vpid해시 키 (엔트리당 VPID 하나).
slot소유자 DWB_SLOT * — 첫 번째 삽입 전까지 NULL; §5.4 포인터 교체의 대상.
mutex엔트리별 잠금; 하나의 VPID에 대한 find/insert/delete 전반에 걸쳐 유지.
stack, nextcubthread::lockfree_hashmap의 freelist / 해시 체인 링크.
del_idlock-free 회수(reclamation)에 사용되는 삭제 트랜잭션 ID.

slots_entry_Descriptor에 등록된 콜백 여섯 개:

콜백동작
dwb_slots_hash_entry_allocmalloc + pthread_mutex_init; 실패 시 NULL/ER_OUT_OF_VIRTUAL_MEMORY.
dwb_slots_hash_entry_freepthread_mutex_destroy + free; NULL이면 ER_FAILED.
dwb_slots_hash_entry_initVPID_SET_NULL (&vpid)slot = NULLfind_or_insert 전의 초기 빈 상태.
dwb_slots_hash_key_copyVPID_COPY (dest, src).
dwb_slots_hash_compare_keyVPID_EQ이면 0, 아니면 -1.
dwb_slots_hash_key(pageid | (volid << 24)) % hash_table_size — volid를 상위 비트에 접어 볼륨들이 버킷에 고르게 분산되도록.

descriptor의 duplicate / insert 콜백 슬롯은 NULL(“삽입 없음”)이다. 채움 순서: find_or_insertkey_copyvpid를 설정하고, dwb_slots_hash_insertslot을 설정한다.

5.7 dwb_get_next_block_for_flush — 채움 순서 게이트

섹션 제목: “5.7 dwb_get_next_block_for_flush — 채움 순서 게이트”

dwb_add_page는 블록이 가득 찼다는 결정을 내리고, 이 함수는 flush 기계가 다음에 어느 블록을 가져갈지 결정한다.

// dwb_get_next_block_for_flush -- src/storage/double_write_buffer.cpp
*block_no = DWB_NUM_TOTAL_BLOCKS; /* <- sentinel: "nothing to flush" */
if (dwb_Global.blocks[dwb_Global.next_block_to_flush].count_wb_pages != DWB_BLOCK_NUM_PAGES)
return; /* <- next block not full yet */
*block_no = dwb_Global.next_block_to_flush;

두 분기가 있다. next_block_to_flush가 아직 가득 차지 않았으면 센티넬 DWB_NUM_TOTAL_BLOCKS를 반환한다. 그보다 뒤에 있는 블록이 가득 찼더라도 앞서 flush할 수 없다. 순서를 어기면 크래시 복구 일관성이 깨지기 때문이다(Chapter 9). 가득 찬 경우에는 해당 블록의 인덱스를 반환하고, daemon(Chapter 7)이 flush 후 next_block_to_flush를 전진시킨다. 이 구조 덕분에 §5.3의 wakeup에서 이전 블록을 기다릴 필요가 없다. daemon이 여기서 다시 확인해 순서상 다음인 가득 찬 블록만 가져가기 때문이다. DWB_BLOCK_NUM_PAGES 비교는 dwb_add_page의 것과 동일하므로 “가득 찼다”는 두 관점이 항상 일치한다.

  1. dwb_add_page는 공개와 계산을 담당한다. *p_dwb_slot == NULL이면 슬롯을 재획득하고(DWB를 사용할 수 없으면 null 슬롯으로 NO_ERROR 반환), dwb_slots_hash_insert로 공개한 뒤, count_wb_pages를 원자적으로 증가시켜 블록 가득 참 여부에 따라 분기한다.
  2. 무효화는 하나의 원시 연산이다: VPID_SET_NULL 더하기 fileio_initialize_res (§5.1). VPID를 null로 만들면 decache가 슬롯을 건너뛰고, io_page를 재초기화하면 재실행(replay) 시 무해하다. !inserted 경로와 LSA_EQ 같은 블록 경로 모두 이를 적용한다.
  3. dwb_slots_hash_insert는 LSA로 충돌을 해결한다. LSA_LT는 더 최신인 기존 소유자를 유지하고 inserted == false로 조기 반환한다. LSA_EQ(로깅 없이 수정됨)는 교체하며, 같은 블록이면 기존 슬롯을 무효화한다. 다른 블록 충돌은 디버그 빌드(#if !defined (NDEBUG))에서만 순서 assert를 실행한다. 더 최신 LSA는 교체한다. 승자는 항상 inserted == true를 보고한다.
  4. 포인터 교체가 STAGED -> HASHED 가시성 전환점이다. slots_hash_entry->slot = slot은 엔트리 mutex 아래 마지막 쓰기로, 읽기 측이 절반만 공개된 슬롯을 볼 수 없다. ensure_metadata는 교체 전반에 걸쳐 OR 병합된다.
  5. dwb_slots_hash_deleteslots_hash_entry->slot == slot일 때만 삭제한다. null VPID 슬롯, 없는 엔트리, 더 최신 슬롯이 소유한 엔트리는 모두 no-op이다.
  6. flush 분기는 실행 모드에 따라 다르다. SERVER_MODE에서 daemon이 있으면 flush 지연 없이 daemon을 깨운다. 그 외에는 dwb_flush_block (..., file_sync_helper_can_flush = false, NULL)로 인라인 flush한다.
  7. dwb_get_next_block_for_flush는 채움 순서를 강제한다. next_block_to_flush가 정확히 가득 찼을 때만 해당 블록을 내어주고, 그렇지 않으면 DWB_NUM_TOTAL_BLOCKS 센티넬을 반환한다.

Chapter 6: DWB 볼륨 및 홈으로의 블록 플러시

섹션 제목: “Chapter 6: DWB 볼륨 및 홈으로의 블록 플러시”

독자 질문: 블록 전체를 플러시할 때, 홈 페이지에 쓰기 전에 DWB에 페이지를 내구적으로 기록하는 정확한 쓰기-fsync 시퀀스는 무엇이며, 찢긴 홈 페이지를 항상 복구 가능하게 만드는 방법은 무엇인가?

동반 문서(cubrid-double-write-buffer.md)는 페이지가 홈보다 먼저 DWB에 내구적으로 도달해야 하는지를 설명한다. 이 챕터는 어떻게dwb_flush_block, dwb_write_block, 그리고 다섯 가지 헬퍼 함수를 — 분기별로 추적한다. 블록 전체를 플러시 데몬에 넘기는 프로듀서(page-buffer flush path)는 이 챕터에서 재도출하지 않는다 — cubrid-page-buffer-manager-detail.md를 참고한다. 두 가지 쓰기 프리미티브 fileio_write / fileio_write_pagesfileio_synchronize fsync 래퍼는 디스크 매니저에 속한다 — 재시도, 부분 쓰기 처리, O_DIRECT vs 버퍼드 모드의 내부 동작은 cubrid-disk-manager-detail.md를 참고한다. 이 챕터에서는 이들을 “이 페이지들을 쓰라”와 “이 파일 디스크립터를 fsync하라”는 원자 연산으로 간주한다. 새로운 구조체는 없다: Ch 1의 DWB_BLOCK, Ch 1·4의 DWB_SLOT을 재사용하고, FLUSH_VOLUME_INFO를 새로 소개한다.

6.1 플러시 시점의 볼륨 레코드: FLUSH_VOLUME_INFO

섹션 제목: “6.1 플러시 시점의 볼륨 레코드: FLUSH_VOLUME_INFO”

블록의 페이지들은 여러 데이터 볼륨에 걸쳐 있을 수 있다. 플러셔는 홈에 페이지를 쓴 뒤 각 볼륨을 정확히 한 번 fsync해야 하며, 두 스레드가 동일한 볼륨을 중복 sync하지 않도록 그 fsync를 헬퍼에 위임할 수 있어야 한다. FLUSH_VOLUME_INFO가 이 조율을 경쟁 없이 가능하게 한다.

// flush_volume_info -- src/storage/double_write_buffer.cpp
struct flush_volume_info
{
int vdes; /* The volume descriptor. */
volatile int num_pages; /* The number of pages to flush in volume. */
volatile bool all_pages_written; /* True, if all pages written. */
volatile bool metadata; /* Include metadata when syncing */
volatile FLUSH_VOLUME_STATUS flushed_status; /* Flush status. */
};
필드역할존재 이유
vdes이 레코드가 sync하는 OS 파일 디스크립터fsync 대상. dwb_add_volume_to_block_flush_area에서 vol_fd 인수로 설정되며, 상위에서 fileio_get_volume_descriptor로 획득한 값이다.
num_pages홈에 써졌지만 아직 fsync되지 않은 페이지 수헬퍼가 이미 0으로 만든 볼륨을 플러셔가 건너뛸 수 있게 하고, max_pages_to_sync로 큰 볼륨을 헬퍼에 위임할 수 있게 한다. volatile: 플러셔가 쓰고, 헬퍼가 읽는다.
all_pages_written이 볼륨의 모든 페이지가 쓰여진 뒤 true가 됨프로듀서-컨슈머 게이트: 플러셔가 더 이상 페이지를 추가하지 않겠다는 약속이 내려지기 전까지 헬퍼는 fsync해선 안 된다.
metadatafileio_synchronize에 전달되는 ensure_metadata 인수볼륨이 확장된 경우 inode/size 메타데이터도 함께 sync해야 한다. 해당 볼륨을 건드리는 모든 슬롯에 걸쳐 OR 연산된다.
flushed_status이 볼륨의 fsync를 어느 스레드가 선점했는지를 나타냄3-상태 열거형(VOLUME_NOT_FLUSHED / VOLUME_FLUSHED_BY_DWB_FILE_SYNC_HELPER_THREAD / VOLUME_FLUSHED_BY_DWB_FLUSH_THREAD). CAS 중재자로서 정확히 한 번만 fsync되도록 보장한다. 플러셔와 헬퍼(Ch 7) 모두 NOT_FLUSHED에서 자신의 값으로 CAS를 시도하며, 이긴 쪽이 fsync하고 진 쪽은 건너뛴다.
flowchart LR
  blk["DWB_BLOCK"] -->|"flush_volumes_info[0..count]"| fvi["볼륨별 FLUSH_VOLUME_INFO"]
  fvi -->|"flushed_status CAS"| arb["플러셔 vs 헬퍼 중재"]
  fvi -->|"num_pages / all_pages_written"| handoff["프로듀서-컨슈머 게이트"]

Figure 6-1. 블록은 볼륨별 플러시 레코드 배열을 소유하며, 각 레코드가 해당 볼륨의 fsync를 중재한다.

불변 조건 (볼륨당 정확히 한 번 fsync). {플러셔, 헬퍼} 중 최대 하나만 각 레코드의 fileio_synchronize를 실행한다. 이는 flushed_status에 대한 ATOMIC_CAS_32VOLUME_NOT_FLUSHED에서부터 강제된다. 이 조건이 깨지면 두 스레드가 동일한 디스크립터를 fsync하게 되고, 승자가 ATOMIC_TAS_32로 0으로 만든 num_pages가 이중 계산된다. CAS 덕분에 num_pages == 0 -> 건너뜀 단축 경로가 안전하다. (이 동일한 CAS의 헬퍼 측은 Ch 7에서 상세히 다룬다.)

6.2 정렬된 스냅샷 구축: dwb_block_create_ordered_slots 및 관련 함수

섹션 제목: “6.2 정렬된 스냅샷 구축: dwb_block_create_ordered_slots 및 관련 함수”

dwb_flush_block은 먼저 슬롯 배열의 VPID 정렬된 프라이빗 사본을 구축한다. 이렇게 하면 (a) 동일 볼륨의 페이지들이 연속으로 배치되어 디스크립터 조회와 fsync가 각각 한 번씩만 필요하고, (b) 중복 VPID가 인접하게 위치하여 오래된 사본을 버릴 수 있다.

// dwb_block_create_ordered_slots -- src/storage/double_write_buffer.cpp
p_local_dwb_ordered_slots = (DWB_SLOT *) malloc ((block->count_wb_pages + 1) * sizeof (DWB_SLOT)); /* +1 sentinel */
if (p_local_dwb_ordered_slots == NULL) { /* er_set OOM */ return ER_OUT_OF_VIRTUAL_MEMORY; } /* only error path */
memcpy (p_local_dwb_ordered_slots, block->slots, block->count_wb_pages * sizeof (DWB_SLOT));
dwb_init_slot (&p_local_dwb_ordered_slots[block->count_wb_pages]); /* sentinel at index count_wb_pages */
qsort (p_local_dwb_ordered_slots, block->count_wb_pages, sizeof (DWB_SLOT), dwb_compare_slots);
*p_ordered_slots_length = block->count_wb_pages + 1; /* length INCLUDES the sentinel */

두 가지 분기가 있다: malloc 실패 시 OOM을 반환하고(호출자 dwb_flush_blockER_FAILED로 변환한 뒤 goto end), 그 외에는 사본을 정렬하여 반환한다. qsort는 처음 count_wb_pages 항목만 정렬하므로 센티넬은 높은 위치에 고정된다. dwb_init_slot은 해당 슬롯을 센티넬로 만든다 — NULL VPID, LSA, io_page(slot->io_page = NULL; VPID_SET_NULL (&slot->vpid); LSA_SET_NULL (&slot->lsa);) — 이로써 dwb_write_block[i + 1] 프로브가 별도의 경계 검사 없이 안전하게 실행된다.

dwb_compare_slots(volid, pageid, lsa.pageid, lsa.offset) 순서로 사전식 정렬을 수행한다. LSA 타이브레이크는 중요하다: 같은 VPID를 가진 두 슬롯은 오래된 사본(더 작은 LSA)이 낮은 인덱스에 위치하며, dedup 루프는 이를 기반으로 더 새로운 사본을 보존한다.

불변 조건 (센티넬이 있는 전체 정렬 순서). 인덱스 0..count_wb_pages-1dwb_compare_slots 기준으로 비감소 순서다; 인덱스 count_wb_pages는 NULL 센티넬(qsort + dwb_init_slot으로 강제됨). 비-NULL 센티넬이면 dwb_write_blockassert (VPID_ISNULL (&p[i+1].vpid) || VPID_LT (...)) 검증이 마지막 실제 페이지에서 실패한다.

분기 완전 번호 순 설명 (S0–S8). 네 곳goto end 지점이 있다 — (1) ordered snapshot 구축 OOM (S1), (2) DWB 볼륨 fileio_write_pages가 NULL 반환 (S5), (3) DWB 볼륨 fileio_synchronize 불일치 (S5), (4) dwb_write_block이 non-NO_ERROR 반환 (S6) — 이 네 곳과 정상 fall-through 모두 end:에 도달한다. 내구성 배리어는 S5다.

S0은 단일 플러셔를 assert한다(ATOMIC_INC_32 (&dwb_Global.blocks_flush_counter, 1); assert (dwb_Global.blocks_flush_counter <= 1);). 대응하는 -1end:에서 이루어지며, 모든 goto end 분기를 포함한 모든 종료 경로에서 실행된다 — 이것이 에러 경로들이 return 대신 jump를 사용하는 이유다. S1은 정렬된 스냅샷을 구축한다(6.2 참고). 실패 시 error_code = ER_FAILED; goto end. S2는 아래의 dedup 루프로, i = 0 .. count_wb_pages - 2 범위(for (i = 0; i < count_wb_pages - 1; i++))를 순회한다. 따라서 s2 = &p[i+1]은 최대 마지막 실제 슬롯에 도달하며, 루프는 센티넬을 건드리지 않는다. S3 (SERVER_MODE)은 file_sync_helper_block != NULL인 동안 스핀하여, 이전 블록이 내구적으로 완료된 뒤에야 이번 블록의 홈 쓰기가 시작되도록 보장한다: 헬퍼가 살아있으면 thread_sleep(1) 후 재시도; 헬퍼가 완료되면 플러셔가 직접 이전 블록의 볼륨을 fsync하고(if (ATOMIC_INC_32(&num_pages, 0) >= 0) 가드는 사실상 항상 참이어서 모든 볼륨이 sync됨) ATOMIC_TAS_ADDRfile_sync_helper_block을 NULL로 만들어 루프를 종료한다. S4ATOMIC_TAS_32 (&count_flush_volumes_info, 0)all_pages_written = false로 초기화한다.

// dwb_flush_block (dedup, S2) -- src/storage/double_write_buffer.cpp
if (!VPID_ISNULL (&s1->vpid) && VPID_EQ (&s1->vpid, &s2->vpid))
{
assert (LSA_LE (&s1->lsa, &s2->lsa)); /* <- s1 is the OLDER copy (sort order) */
VPID_SET_NULL (&s1->vpid); /* drop from the ordered snapshot */
VPID_SET_NULL (&(block->slots[s1->position_in_block].vpid)); /* and from the LIVE block */
fileio_initialize_res (thread_p, s1->io_page, IO_PAGESIZE); /* reinit so the DWB-volume write emits a clean page */
s2->ensure_metadata = s1->ensure_metadata || s2->ensure_metadata; /* carry metadata forward */
}

dedup 분기는 인접한 두 슬롯의 VPID가 같을 때만 실행된다. 세 가지 작업을 수행한다: 오래된 사본을 스냅샷과 라이브 블록 양쪽에서 null 처리하고(홈 쓰기 시 dwb_write_block이 건너뛰고, 해시 삭제 패스도 생존 슬롯을 그대로 둔다), 버려진 슬롯의 io_pagefileio_initialize_res로 재초기화하며(S5가 write_buffer 전체를 DWB 볼륨에 쓰기 때문에 — 버려진 페이지는 오래된 바이트가 아닌 깨끗이 초기화된 페이지여야 한다), ensure_metadata를 생존 슬롯으로 전달한다. #if !defined(NDEBUG) WAL 검사(logpb_need_wal을 페이지 LSA에 적용)가 같은 루프 본문에 있지만, 디버그 전용 프로브이며 런타임 분기에는 영향을 주지 않는다.

S5 — DWB 볼륨 쓰기 + fsync (배리어).

// dwb_flush_block (DWB write+sync, S5) -- src/storage/double_write_buffer.cpp
if (fileio_write_pages (thread_p, dwb_Global.vdes, block->write_buffer, 0, block->count_wb_pages,
IO_PAGESIZE, FILEIO_WRITE_NO_COMPENSATE_WRITE) == NULL)
{ assert (false); error_code = ER_FAILED; goto end; }
/* ... perfmon PSTAT_PB_NUM_IOWRITES += count_wb_pages ... */
if (fileio_synchronize (thread_p, dwb_Global.vdes, dwb_Volume_name, false) != dwb_Global.vdes)
{ assert (false); error_code = ER_FAILED; goto end; } /* <- DURABILITY BARRIER */

write_buffer 전체(방금 재초기화된 dedup 페이지를 포함해 무해한 깨끗한 페이지로 기록됨)가 단일 DWB 볼륨에 하나의 fileio_write_pages 호출로 기록된다(cubrid-disk-manager-detail.md에서 재시도/부분 쓰기 처리 방법 참고). NULL 반환과 dwb_Global.vdes 이외의 값 반환이 두 goto end 분기다. DWB 볼륨 fsync는 ensure_metadata = false를 전달하는데, DWB 볼륨은 고정 크기로 사전에 생성되기 때문이다 — 홈 볼륨만 확장될 수 있다. S6에서 dwb_write_block (thread_p, block, p_dwb_ordered_slots, ordered_slots_length, file_sync_helper_can_flush, /*remove_from_hash=*/true)를 호출한다(6.4 참고). 에러 시 assert (false); goto end.

불변 조건 (내구성 배리어 — DWB 존재의 이유). dwb_Global.vdesfileio_synchronize는 첫 번째 홈 쓰기가 시작되기 전에 반드시 완료되어야 한다 — 직선적인 시퀀싱으로 강제된다(dwb_write_block이 바로 다음 문장이며, sync의 에러 분기 goto end가 이를 건너뛴다). 이 조건이 깨지면 홈 쓰기 중간의 크래시로 홈 페이지가 찢기는데, DWB 사본도 아직 내구적이지 않아 복구(Ch 9)가 손상된 홈 페이지와 유효한 DWB 사본 없이 직면하게 된다. 배리어가 유지되는 한, 크래시 시점에 쓰이고 있거나 곧 쓰일 모든 홈 페이지는 DWB 볼륨에 완전하고 fsync된 쌍둥이 사본이 존재한다. Ch 9의 재시작 스캔이 그 쌍둥이를 찢긴 홈에 복사해 torn-page 복구를 항상 가능하게 한다.

S7 — 홈 볼륨별 fsync (fvi는 아래에서 block->flush_volumes_info의 약자):

// dwb_flush_block (step-7 fsync loop) -- src/storage/double_write_buffer.cpp
max_pages_to_sync = prm_get_integer_value (PRM_ID_PB_SYNC_ON_NFLUSH) / 2;
for (i = 0; i < block->count_flush_volumes_info; i++)
{
assert (fvi[i].vdes != NULL_VOLDES);
num_pages = ATOMIC_INC_32 (&fvi[i].num_pages, 0);
if (num_pages == 0) continue; /* (a) already flushed by helper */
#if defined (SERVER_MODE)
if (file_sync_helper_can_flush)
{ if (num_pages > max_pages_to_sync && dwb_is_file_sync_helper_daemon_available ()) continue; } /* (b) offload */
else { assert (dwb_Global.file_sync_helper_block == NULL); } /* (b') no helper offload possible */
#endif
if (!ATOMIC_CAS_32 (&fvi[i].flushed_status, VOLUME_NOT_FLUSHED, VOLUME_FLUSHED_BY_DWB_FLUSH_THREAD))
continue; /* (c) helper already claimed it */
num_pages = ATOMIC_TAS_32 (&fvi[i].num_pages, 0); /* zero so the helper skips this volume */
(void) fileio_synchronize (thread_p, fvi[i].vdes, NULL, fvi[i].metadata);
}

세 개의 continue — (a)/(b)/(c) — 는 인라인으로 주석 처리되어 있다. (a)와 (c)는 모두 헬퍼가 먼저 도달했을 때 실행된다 — (a)는 소진된 카운터를 보고, (c)는 status CAS에서 진다. (b’)의 else 분기(SA_MODE 또는 데몬 없음처럼 file_sync_helper_can_flush가 false일 때)는 헬퍼가 없음을 assert하므로, 남아있는 모든 볼륨이 인라인으로 fsync된다. 플러셔가 볼륨을 선점하면 fsync 전에 ATOMIC_TAS_32num_pages를 0으로 만들어 헬퍼의 num_pages == 0 체크가 건너뛰도록 한다.

S8 — 고정 순서로 완료 게시: all_pages_written = true (S7 종료 시, 헬퍼가 마무리할 수 있도록) -> ATOMIC_TAS_32 (&count_wb_pages, 0) -> ATOMIC_INC_64 (&version, 1ULL) -> WRITE-STARTED 비트 클리어 -> next_block_to_flush 진행 -> dwb_signal_block_completion -> *current_position_with_flags 저장. 두 CAS 지점은 재시도 정책이 다르다: 공유 position word에서 WRITE-STARTED 비트를 클리어하는 것(Ch 2, DWB_ENDS_BLOCK_WRITING 경유)은 동시 프로듀서가 해당 word를 건드릴 수 있어 reset_bit_position: 재시도 루프를 사용한다; next_block_to_flush 진행은 CAS로, 실패 분기에서 assert_release (false)를 실행한다 — 단일 플러셔만이 이를 진행시키므로 반드시 이겨야 한다. dwb_signal_block_completion은 대기 큐를 해제하고 블록된 스레드를 깨운다(Ch 8); version++는 오래된 리더(Ch 8)에게 블록이 재활용되었음을 알린다.

S end — 정리. fall-through와 모든 goto end에서 도달: ATOMIC_INC_32 (&blocks_flush_counter, -1), 스냅샷이 할당된 경우 free_and_init, PSTAT_DWB_FLUSH_BLOCK_TIME_COUNTERS 충전, return error_code.

flowchart TD
  s1["S1 정렬된 스냅샷 구축"] --> s2["S2 인접한 동일-VPID dedup"]
  s2 --> s3["S3 이전 블록 내구성 대기"]
  s3 --> s5["S5 DWB 볼륨 쓰기+fsync -- 배리어"]
  s5 --> s6["S6 dwb_write_block: 홈으로 페이지 분산 쓰기"]
  s6 --> s7["S7 홈 볼륨별 fsync"]
  s7 --> s8["S8 완료 게시, version++, signal"]
  s5 -.->|"sync 실패"| e["end: 카운터--, free, return"]
  s8 --> e

Figure 6-2. 플러시 시퀀스. S5 이후의 모든 작업(홈 쓰기 + 홈 fsync)은 S5에서 fsync된 DWB 사본으로 복구 가능하다.

6.4 dwb_write_block — 홈 페이지 쓰기

섹션 제목: “6.4 dwb_write_block — 홈 페이지 쓰기”

dwb_write_block은 스냅샷을 순회하며 살아있는 각 페이지를 홈에 쓰고, 볼륨 경계에서 flush_volumes_info[]를 구축하며, 기회가 되면 볼륨을 헬퍼에 위임한다. 페이지별 루프(모든 분기 포함):

  1. NULL VPID (dedup된 슬롯 또는 후행 센티넬): continue — 쓰지 않고 카운트하지 않는다.
  2. 새 volid: 이전 레코드를 닫고(all_pages_written = true, can_flush_volume 올림, current_flush_volume_info = NULL 초기화), fileio_get_volume_descriptor를 호출한다. 드롭된 볼륨(NULL_VOLDES)은 레코드를 열지 않고 continue — 페이지는 조용히 건너뛴다. 그 외에는 dwb_add_volume_to_block_flush_area로 새 레코드를 연다.
  3. 같은 volid: current_flush_volume_info->metadata = metadata || ensure_metadata.
  4. fileio_write HOME (NO_COMPENSATE_WRITE; 디스크 매니저 프리미티브; cubrid-disk-manager-detail.md 참고). 유일한 NULL 반환 시 return ER_FAILED를 직접 수행한다(not goto end) — dwb_flush_block이 이를 자신의 goto end로 변환한다.
  5. Handoff (server mode), 하나의 동시성 세부 사항:
// dwb_write_block (handoff) -- src/storage/double_write_buffer.cpp
ATOMIC_INC_32 (&current_flush_volume_info->num_pages, 1); /* count this home write; count_writes++ too */
if (file_sync_helper_can_flush && (count_writes >= num_pages_to_sync || can_flush_volume == true)
&& dwb_is_file_sync_helper_daemon_available ())
{
if (ATOMIC_CAS_ADDR (&dwb_Global.file_sync_helper_block, (DWB_BLOCK *) NULL, block)) /* claim only if NULL */
dwb_file_sync_helper_daemon->wakeup ();
/* then reset count_writes = 0, can_flush_volume = false */
}

루프 뒤에 세 가지 후속 동작이 있다: 마지막으로 열린 볼륨을 닫고(all_pages_written = true); 작은 블록으로 count_writes가 임계값에 도달하지 않은 경우를 위한 두 번째 헬퍼 깨우기(file_sync_helper_block == NULL && count_flush_volumes_info > 0 조건); 그리고 remove_from_hash가 설정된 경우 해시 삭제 패스(6.5).

불변 조건 (볼륨 레코드는 페이지 카운트보다 먼저 게시됨). dwb_add_volume_to_block_flush_area는 레코드를 게시(count_flush_volumes_infoATOMIC_INC_32)한 뒤에야 첫 번째 fileio_write/num_pages++에 사용할 레코드 포인터를 반환하므로, 헬퍼가 배열 범위 밖의 레코드에서 num_pages를 읽어 페이지를 놓치는 일이 발생하지 않는다.

6.5 해시 삭제 패스와 remove_from_hash

섹션 제목: “6.5 해시 삭제 패스와 remove_from_hash”

remove_from_hash가 true일 때(dwb_flush_block의 호출에서는 항상 그러하다), dwb_write_block의 후미에서 살아남은 각 슬롯을 전역 VPID→slot 해시에서 삭제한다. 이로써 미래의 프로듀서가 플러시된 페이지를 더 이상 찾지 못하게 된다. 루프는 스냅샷 사본이 아닌 라이브 block->slots[p_dwb_ordered_slots[i].position_in_block]를 대상으로 dwb_slots_hash_delete를 호출한다. 아래의 slot == slot 동일성 검사가 해시에 저장된 정확한 주소를 비교할 수 있도록 하기 위해서다:

// dwb_slots_hash_delete -- src/storage/double_write_buffer.cpp
vpid = &slot->vpid;
if (VPID_ISNULL (vpid))
return NO_ERROR; /* deduped slot: never in hash, nothing to do */
slots_hash_entry = dwb_Global.slots_hashmap.find (thread_p, *vpid);
if (slots_hash_entry == NULL)
return NO_ERROR; /* already removed by someone else */
if (slots_hash_entry->slot == slot) /* still OUR slot -> erase under lock */
{ if (!dwb_Global.slots_hashmap.erase_locked (thread_p, *vpid, slots_hash_entry)) { assert_release (false); ... } }
else
pthread_mutex_unlock (&slots_hash_entry->mutex); /* a newer slot owns this VPID now: just unlock */

세 가지 분기(인라인 주석): NULL VPID 건너뜀; 항목 없음 건너뜀; 항목 발견 — slots_hash_entry->slot == slot인 경우(아직 정확한 슬롯)에만 erase_locked, 그렇지 않으면 해시가 VPID를 다른 슬롯(더 최신 프로듀서가 새로운 사본으로 재바인딩한 것)에 매핑하므로 삭제하지 않고 잠금만 해제한다. 이 else 분기는 플러셔가 동시에 쓰인 다른 슬롯의 항목을 삭제하지 못하도록 막는다. 소스 측 guard는 발견된 항목의 VPID가 NULL이거나 우리 것과 같은지(VPID_ISNULL (&entry->slot->vpid) || VPID_EQ (...)) 동일성 검사 전에 assert한다. find는 항목 mutex를 보유한 채 반환한다; erase_locked 경로와 else 경로 모두 이를 해제한다(erase_locked는 내부적으로; else는 명시적 pthread_mutex_unlock으로).

6.6 dwb_add_volume_to_block_flush_area와 dwb_compare_vol_fd

섹션 제목: “6.6 dwb_add_volume_to_block_flush_area와 dwb_compare_vol_fd”

dwb_add_volume_to_block_flush_area는 하나의 레코드를 추가하고 원자적으로 게시한다:

// dwb_add_volume_to_block_flush_area -- src/storage/double_write_buffer.cpp
flush_new_volume_info = &block->flush_volumes_info[block->count_flush_volumes_info];
flush_new_volume_info->vdes = vol_fd;
flush_new_volume_info->num_pages = 0;
flush_new_volume_info->all_pages_written = false;
flush_new_volume_info->metadata = ensure_metadata;
flush_new_volume_info->flushed_status = VOLUME_NOT_FLUSHED;
ATOMIC_INC_32 (&block->count_flush_volumes_info, 1); /* publish AFTER all fields are set */
return flush_new_volume_info;

모든 필드를 설정한 뒤 카운트를 원자적으로 증가시킨다 — 소스 주석은 “코드 재정렬을 방지하기 위해” atomic을 사용한다고 명시한다(쓰는 쪽은 하나, 읽는 쪽은 둘: 플러셔와 헬퍼). 데몬이 절반만 구성된 원소를 보는 일이 없도록 한다. 후미의 assert는 카운트가 정확히 1 증가했고 max_to_flush_vdes 이하(블록 생성 시 배열 크기, Ch 3)임을 확인한다.

dwb_compare_vol_fd(return ((*vol_fd1) - (*vol_fd2));)는 단순한 int 디스크립터 qsort 비교자다. 이는 플러시 경로에서 호출되지 않는다(이 개정 시점에서 double_write_buffer.cpp 내에 호출자가 없다). 디스크립터 배열 비교자로 보존되어 있으며, 동일한 flush-area 북키핑 패밀리에 속하기 때문에 여기서 언급할 뿐이다. dwb_flush_block 또는 dwb_write_block에서 이를 호출한다고 추론하지 말 것.

  1. 내구성 배리어는 단 한 줄이며, 그것이 DWB가 존재하는 이유다. write_buffer 전체가 하나의 fileio_write_pages(NO_COMPENSATE_WRITE)로 단일 DWB 볼륨에 기록되고, 바로 이어지는 fileio_synchronize (dwb_Global.vdes, ...)dwb_write_block이 홈으로 페이지를 분산 쓰기 시작하기 전에 엄격히 순서가 보장된다. sync의 에러 분기 goto end는 홈 쓰기를 건너뛴다. DWB 사본이 먼저 fsync-내구적이 되므로, 크래시 이후 찢긴 홈 페이지가 발견되면 그 깨끗한 쌍둥이를 복원할 수 있다(Ch 9).
  2. 정렬된 스냅샷은 두 가지 역할을 동시에 수행한다. (volid, pageid, lsa) 기준 정렬로 볼륨이 연속으로 배치되어(각각 한 번의 fsync) 같은 VPID 중복본이 인접하게 위치한다. dedup 루프는 더 높은 LSA 사본을 보존하고, 오래된 것을 스냅샷과 라이브 블록 양쪽에서 null 처리하며, fileio_initialize_res로 버려진 페이지를 재초기화해 DWB 쓰기가 깨끗한 페이지를 내보내도록 한다. count_wb_pages 위치의 NULL 센티넬은 [i+1] 프로브를 경계 안전하게 만든다.
  3. fsync는 3-상태 CAS로 볼륨당 정확히 한 번 실행된다. flushed_status + ATOMIC_CAS_32가 플러셔와 헬퍼를 중재한다. num_pages == 0max_pages_to_sync 임계값이 큰 볼륨을 헬퍼에 위임하며, CAS에서 진 쪽은 건너뛴다. 레코드는 모든 필드가 설정된 뒤에만 게시되며, 해시 삭제 패스(dwb_write_block 내부, dwb_flush_block이 아님)는 해당 항목이 아직 라이브 슬롯을 가리킬 때만 삭제한다.
  4. 완료는 고정 순서로 게시된다. all_pages_written -> count_wb_pages = 0 -> version++ -> WRITE 비트 클리어(CAS 재시도 루프) -> next_block_to_flush 진행(실패 분기가 assert_release (false)인 CAS) -> dwb_signal_block_completion. version 증가는 오래된 리더(Ch 8)에게 블록이 재활용되었음을 알린다.
  5. 두 가지 쓰기 프리미티브와 플러시 프로듀서는 빌려온 것이며 재도출하지 않는다. fileio_write / fileio_write_pages / fileio_synchronize는 디스크 매니저(cubrid-disk-manager-detail.md)에 속한다. 블록을 채우고 위임하는 page-buffer flush path는 cubrid-page-buffer-manager-detail.md에 있다. 볼륨별 fsync CAS를 공유하는 헬퍼 스레드는 Ch 7에서 다룬다.

Chapter 7: 데몬 플러시 드라이버와 파일 동기화 헬퍼

섹션 제목: “Chapter 7: 데몬 플러시 드라이버와 파일 동기화 헬퍼”

DWB는 두 개의 백그라운드 스레드로 스스로를 비워낸다. "dwb-flush-block" 으로 등록된 플러시 드라이버(1 ms 루퍼)는 가득 찬 블록을 하나 골라 쓰기-동기화 파이프라인 전체를 수행하고, "dwb-file-sync" 로 등록된 헬퍼(10 ms 루퍼)는 가장 느린 부분 — 홈 볼륨별 fileio_synchronize 호출 — 을 분리해 처리한다. 이 덕분에 플러셔는 디스크 I/O에 묶이지 않고 후속 라이터들이 DWB 볼륨을 계속 채울 수 있다. 인계(handoff)는 단일 포인터 (dwb_Global.file_sync_helper_block)와 볼륨별 원자적 상태 필드 하나로 이루어진다. 이 장에서는 그 인계 구조, 데몬 본체, 그리고 가용성 판별 조건을 해부한다.

교차 참조: double-buffering과 torn-page 이론은 상위 개요 문서 cubrid-double-write-buffer.md에, 블록 채우기와 dwb_flush_block 본체는 5장과 6장에 있다.

두 데몬은 모두 모듈 전역 cubthread::daemon * 포인터(dwb_flush_block_daemon, dwb_file_sync_helper_daemon)이며, #if defined (SERVER_MODE) 아래에서만 정의되고 생성 전까지는 NULL이다. 이들에 접근하는 모든 경로는 PRM_ID_ENABLE_DWB_FLUSH_THREAD로 게이트되며, 이 파라미터가 꺼져 있으면 DWB는 라이터 스레드에서 인라인 플러시로 동작하는 축소 모드로 전환된다. 각 데몬 본체 내부에는 두 번째 게이트인 BO_IS_FLUSH_DAEMON_AVAILABLE()이 있어 boot_Enabled_flush_daemons(부트·재시작 중에 클리어됨)를 읽는다. 두 조건이 모두 충족되어야 한다: 서버가 부트를 마쳤고, 그리고 운영자가 명시적으로 활성화했어야 한다.

Figure 7-1은 인계 슬롯 주변의 관계를 보여준다.

flowchart LR
  FB["dwb-flush-block 데몬<br/>루퍼 1 ms"]
  FH["dwb-file-sync 데몬<br/>루퍼 10 ms"]
  HP["file_sync_helper_block<br/>단일 DWB_BLOCK* 슬롯"]
  HOME["홈 볼륨<br/>fileio_synchronize"]

  FB -->|"CAS NULL to block + wakeup"| HP
  HP -->|"dwb_file_sync_helper가 읽음"| FH
  FH --> HOME
  FB -->|"대체: 헬퍼 미가용 시 인라인 fsync"| HOME

Figure 7-1. 인계 슬롯. 등록된 데몬 이름은 "dwb-flush-block""dwb-file-sync"이다 (후자는 함수명 접두사 dwb_file_sync_helper_*와 달리 "dwb-file-sync-helper"아니다).

7.2 생명주기: 초기화, 루퍼 생성, 소멸

섹션 제목: “7.2 생명주기: 초기화, 루퍼 생성, 소멸”

dwb_daemons_init는 DWB 생성 시 한 번 실행되며, dwb_flush_block_daemon_init() 다음에 dwb_file_sync_helper_daemon_init()을 순서대로 호출해 두 데몬을 생성한다. 각각 cubthread::looper(깨어나는 주기)와 태스크 객체를 만든 뒤 cubthread::get_manager()->create_daemon(looper, task, name)을 호출한다. 두 데몬은 주기와 래퍼 방식에서 차이가 있다:

  • dwb_flush_block_daemon_init: looper(milliseconds(1)) + 전용 dwb_flush_block_daemon_task, 이름 "dwb-flush-block".
  • dwb_file_sync_helper_daemon_init: looper(milliseconds(10)) + 범용 entry_callable_task(dwb_file_sync_helper_execute), 이름 "dwb-file-sync".

각 init 함수 앞에는 파일 스코프에서 REGISTER_DAEMON(...) 매크로가 정적으로 데몬 이름을 스레드 매니저에 등록한다(REGISTER_DAEMON (dwb_flush_block), REGISTER_DAEMON (dwb_file_sync_helper)).

이 비대칭은 의도적이다. 플러셔의 전용 태스크 클래스는 cond-wait 시간을 측정하는 PERF_UTIME_TRACKER를 담고 있는 반면(7.3절), 상태가 없는 헬퍼는 범용 래퍼만으로 충분하다. 주기 차이도 중요하다 — 플러셔는 블록이 꽉 찼을 때 즉각 반응해야 하고(그러지 않으면 라이터들이 빈 슬롯을 기다리며 멈춘다), 헬퍼는 자신에게 넘겨진 볼륨만 처리하면 된다.

dwb_daemons_destroy는 플러셔를 먼저 destroy_daemon(...)으로 종료한다. destroy_daemon은 스레드를 join하므로 반환 시점에 두 데몬 본체가 실행 중인 상태는 없다. 포인터는 NULL이 되지 않고 유지되며, 종료 처리(10장)에서 생산자가 이미 소멸된 데몬을 역참조하지 않도록 보장한다.

dwb_flush_block_daemon_taskcubthread::entry_task를 상속하고, 생성자에서 시작되는 멤버 PERF_UTIME_TRACKER m_perf_track 하나를 갖는다. execute 오버라이드가 매 틱의 진입점이며, 두 게이트를 준수하는 얇은 래퍼다:

// dwb_flush_block_daemon_task::execute -- src/storage/double_write_buffer.cpp
void execute (cubthread::entry &thread_ref) override
{
if (!BO_IS_FLUSH_DAEMON_AVAILABLE ()) /* <- branch A: boot gate closed, do nothing */
{
return;
}
/* performance tracking */
PERF_UTIME_TRACKER_TIME (NULL, &m_perf_track, PSTAT_DWB_FLUSH_BLOCK_COND_WAIT);
/* flush pages as long as necessary */
if (prm_get_bool_value (PRM_ID_ENABLE_DWB_FLUSH_THREAD) == true) /* <- branch B: param gate */
{
if (dwb_flush_next_block (&thread_ref) != NO_ERROR)
{
assert_release (false); /* <- branch C: a flush error is fatal */
}
}
PERF_UTIME_TRACKER_START (&thread_ref, &m_perf_track);
}

PERF_UTIME_TRACKER 쌍은 작업 구간을 감싸 PSTAT_DWB_FLUSH_BLOCK_COND_WAIT 카운터가 유휴 시간만 누적하도록 한다: _TIME은 활성 틱 시작 시점에 클록을 멈추고, _START는 본체가 슬립으로 돌아가기 직전에 재시작한다. assert_release(false)는 실질적 의미를 갖는다 — 플러시 데몬에는 복구 경로가 없으므로, 플러시 오류는 블록을 반쯤 플러시한 채 두는 대신 서버를 종료한다.

dwb_flush_next_block이 실제 드라이버다 — goto start로 구동되는 루프가 준비된 블록을 모두 비운다. 매 이터레이션은 position_with_flags = ATOMIC_INC_64 (&dwb_Global.position_with_flags, 0ULL)를 다시 읽고 분기한다: (1) !DWB_IS_CREATED || DWB_IS_MODIFYING_STRUCTURE이면 return NO_ERROR; (2) dwb_get_next_block_for_flushblock_no < DWB_NUM_TOTAL_BLOCKS를 반환하면 dwb_flush_block (..., true /* file_sync_helper_can_flush */, NULL) 호출; (3) 오류 시 return error_code (호출자가 assert_release(false)로 전환); (4) 성공 시 또 다른 블록이 가득 찰 수 있으므로 goto start; (5) 가득 찬 블록이 없으면 이번 틱은 return NO_ERROR로 종료.

position_with_flags 재독(2장)은 루프를 종료 안전하게 만든다 — 구조 변경은 MODIFYING을 세트하므로 다음 이터레이션이 branch 1에서 탈출한다. branch 2 대 5는 dwb_get_next_block_for_flush가 결정하는데, 이 함수는 먼저 sentinel *block_no = DWB_NUM_TOTAL_BLOCKS를 세트한 뒤 dwb_Global.blocks[next_block_to_flush].count_wb_pages == DWB_BLOCK_NUM_PAGES일 때만 dwb_Global.next_block_to_flush로 덮어쓴다 — 즉 next_block_to_flush만 제공하며, 완전히 가득 찬 경우에만 제공한다.

불변 조건 — 블록은 채워진 순서대로 플러시된다. 데몬은 임의의 가득 찬 블록을 고르지 않고, 오직 next_block_to_flush만 선택한다; 커서는 dwb_flush_block 내부에서(dwb_Global.next_block_to_flush에 대한 ATOMIC_CAS_32 근처, 본체 말미에서) 라운드 로빈으로 전진하므로, 디스크 상의 쓰기 순서는 버퍼링 순서와 일치한다. 크래시 복구 재플레이(9장)는 이 순서에 의존한다 — 순서가 어긋나면 더 최신 페이지 버전이 더 오래된 것보다 먼저 기록되어 복구 시 낡은 데이터가 부활할 수 있다. count_wb_pages 가드는 블록의 모든 슬롯이 기록되었을 때만 인계가 이루어지도록 보장한다. 호출은 file_sync_helper_can_flush = true를 전달해 홈 볼륨 fsync를 헬퍼에게 위임할 수 있는 신호를 준다(본체는 6장, 수신측은 7.4절).

틱 단위 래퍼 dwb_file_sync_helper_executeexecute와 동일하게 두 게이트를 적용한다 — !BO_IS_FLUSH_DAEMON_AVAILABLE()이면 return, 이후 PRM_ID_ENABLE_DWB_FLUSH_THREAD일 때만 dwb_file_sync_helper(&thread_ref) 호출 — 그러나 오류 에스컬레이션이 없다: 반환값을 무시한다(최선 노력 방식; 놓친 볼륨은 플러셔가 인라인으로 동기화, 7.5절). dwb_file_sync_helper는 인계 포인터를 한 번 읽은 뒤 해당 블록의 flush_volumes_info 배열을 순회한다(Figure 7-2).

flowchart TD
  B{"생성됨이고 구조 변경 중 아님?"}
  B -- no --> Z1["return NO_ERROR"]
  B -- yes --> D{"file_sync_helper_block이 NULL?"}
  D -- yes --> Z1
  D -- no --> E["count_flush_volumes_info와 all_pages_written 스냅샷"]
  E --> F["i = start_flush_volume .. count 반복"]
  F --> G{"이미 FILE_SYNC_HELPER?"}
  G -- no --> H{"CAS NOT_FLUSHED → FILE_SYNC_HELPER"}
  H -- fail --> F
  H -- ok --> J["볼륨 소유"]
  G -- yes --> J
  J --> K{"num_pages가 num_pages_to_sync 미만?"}
  K -- "부분, 더 올 예정" --> L["need_wait, break"]
  K -- "충분 OR 소량이지만 완료" --> M["TAS num_pages=0, fileio_synchronize"]
  M --> F
  L --> N["can_flush_volume 재계산"]
  F -->|루프 종료| N
  N --> O{"can_flush_volume?"}
  O -- yes --> E
  O -- "no, need_wait" --> P["thread_sleep 1, can_flush_volume=true"]
  P --> E
  O -- "no, 완료" --> Q["TAS_ADDR file_sync_helper_block=NULL, return"]

Figure 7-2. 헬퍼의 볼륨에 대한 외부 do-while.

두 조기 반환(branch 1 !DWB_IS_CREATED || DWB_IS_MODIFYING_STRUCTURE, branch 2 block == NULL) 이후, 본체는 내부 볼륨 for 루프를 감싸는 외부 do { ... } while (can_flush_volume == true)다. 재시도 세부 사항: need_wait가 세트되고 어떤 볼륨도 플러시하지 못했을 때 헬퍼는 thread_sleep(1)을 수행한 뒤 can_flush_volume = true를 세트해, 슬립 동안 도착한 데이터를 처리하기 위해 do-while을 재진입한다(Figure 7-2 엣지 P --> E).

클레임(claim)이 프로토콜의 핵심이다:

// dwb_file_sync_helper -- src/storage/double_write_buffer.cpp (claim + gating + sync, inner for loop)
if (current_flush_volume_info->flushed_status != VOLUME_FLUSHED_BY_DWB_FILE_SYNC_HELPER_THREAD)
{
if (!ATOMIC_CAS_32 (&current_flush_volume_info->flushed_status, VOLUME_NOT_FLUSHED,
VOLUME_FLUSHED_BY_DWB_FILE_SYNC_HELPER_THREAD))
{
/* Flushed by DWB flusher, skip it. */
assert_release (current_flush_volume_info->flushed_status == VOLUME_FLUSHED_BY_DWB_FLUSH_THREAD);
continue;
}
}
/* I'm the flusher of the volume. */
assert_release (current_flush_volume_info->flushed_status == VOLUME_FLUSHED_BY_DWB_FILE_SYNC_HELPER_THREAD);
num_pages = ATOMIC_INC_32 (&current_flush_volume_info->num_pages, 0);
if (num_pages < num_pages_to_sync)
{
if (current_flush_volume_info->all_pages_written == true)
{
if (num_pages == 0)
{
/* Already flushed. */
continue;
}
/* Needs flushing. */ // ... few-but-complete: fall through and sync ...
}
else
{
/* Not enough pages, check the other volumes and retry. */
assert (all_block_pages_written == false);
if (first_partial_flushed_volume == -1)
{
first_partial_flushed_volume = i;
}
need_wait = true;
break; /* <- more coming, defer this volume and wait */
}
}
else if (current_flush_volume_info->all_pages_written == false)
{
if (first_partial_flushed_volume == -1)
{
first_partial_flushed_volume = i; /* enough now, but revisit: more may arrive */
}
}
/* Reset the number of pages in volume. */
num_pages2 = ATOMIC_TAS_32 (&current_flush_volume_info->num_pages, 0); /* <- claim pages, reset to 0 */
assert_release (num_pages2 >= num_pages); /* <- count only grew between the two reads */
(void) fileio_synchronize (thread_p, current_flush_volume_info->vdes, NULL, current_flush_volume_info->metadata);

불변 조건 — 각 볼륨은 정확히 한 스레드가 fsync한다. flushed_status는 세 가지 상태의 열거형 FLUSH_VOLUME_STATUS (VOLUME_NOT_FLUSHED, VOLUME_FLUSHED_BY_DWB_FILE_SYNC_HELPER_THREAD, VOLUME_FLUSHED_BY_DWB_FLUSH_THREAD)이며, flush_volume_info의 멤버로 vdes, 원자적 num_pages, all_pages_written, metadata와 함께 있다. 헬퍼는 CAS NOT_FLUSHED → FILE_SYNC_HELPER로 클레임하고, 플러셔의 인라인 경로(7.5절, 6장)는 대칭적으로 FLUSH_THREAD로 CAS한다. 먼저 성공한 쪽이 fileio_synchronize를 수행하고, 실패한 쪽은 continue한다. 이를 깨면 볼륨이 두 번 fsync되거나, 원자성 없이는 둘 다 건너뛰어 복구 구멍이 생긴다. 실패 후의 assert_release는 패배한 쪽의 유일한 합법적 상태를 FLUSH_THREAD로 고정한다. TAS 이후의 assert_release(num_pages2 >= num_pages)는 동반 불변 조건을 고정한다: 두 읽기 사이에 동시 라이터는 페이지를 추가할 수만 있고 제거할 수 없다.

클레임 아래의 게이팅(J → K → ... 노드, Figure 7-2)은 두 목표의 균형을 잡는다: 라이터가 아직 스트리밍 중인 볼륨에 일부 더티 페이지만 있을 때 fsync를 하지 않고(break + need_wait), all_pages_written이 세트되면 무한정 대기하지 않는다(num_pages_to_sync = PRM_ID_PB_SYNC_ON_NFLUSH). ATOMIC_TAS_32(..., 0)은 카운트를 조율한다: 0으로 리셋하는 것은 “처리됨”을 의미하고, 동시 라이터가 다시 올리면 first_partial_flushed_volume으로 볼륨이 재방문된다(== -1 가드로 가장 이른 미완성 볼륨을 고정). 루프 이후 start_flush_volume이 전진하고(first_partial_flushed_volume이 있으면 그쪽으로, 없으면 마지막 볼륨을 지나), can_flush_volume/need_wait가 증가했을 수 있는 count_flush_volumes_info 기준으로 재계산된다; 최종 패스에서 ATOMIC_TAS_ADDR로 슬롯을 NULL로 해제한다(block == dwb_Global.file_sync_helper_block assert로 보호).

불변 조건 — 인계 슬롯에는 블록이 최대 하나. file_sync_helper_block은 단일 포인터이지 큐가 아니다. 생산자는 오직 ATOMIC_CAS_ADDR(NULL, block)으로만 설치하며 — NULL이 아닌 슬롯을 절대 덮어쓰지 않는다 — 헬퍼(또는 인라인 폴백, 또는 teardown)만이 클리어한다. 슬롯에 블록이 있는 동안 다른 블록은 넘겨질 수 없으므로, 플러셔의 “이전 블록 동기화 완료 대기” 루프(7.5절)가 블록을 직렬화한다 — 이것이 바로 블록이 채워진 순서대로 플러시되는 이유다(7.3절).

7.5 생산자 측: 깨우기와 인라인 폴백

섹션 제목: “7.5 생산자 측: 깨우기와 인라인 폴백”

플러셔는 헬퍼를 직접 호출하지 않고, 블록을 게시한 뒤 데몬을 깨운다. dwb_write_block 내부(dwb_flush_block에서 구동):

// dwb_write_block -- src/storage/double_write_buffer.cpp (mid-stream wakeup site)
ATOMIC_INC_32 (&current_flush_volume_info->num_pages, 1);
count_writes++;
if (file_sync_helper_can_flush && (count_writes >= num_pages_to_sync || can_flush_volume == true)
&& dwb_is_file_sync_helper_daemon_available ())
{
if (ATOMIC_CAS_ADDR (&dwb_Global.file_sync_helper_block, (DWB_BLOCK *) NULL, block))
{
dwb_file_sync_helper_daemon->wakeup (); /* install only if slot empty, then nudge */
}
/* Add statistics. */
perfmon_add_stat (thread_p, PSTAT_PB_NUM_IOWRITES, count_writes);
count_writes = 0;
can_flush_volume = false;
}

CAS 가드는 생산자 측에서 단일 슬롯 불변 조건을 적용한다: 이전 블록이 여전히 파킹되어 있으면 CAS가 실패하고 플러셔는 작업을 스스로 처리한다. dwb_write_block은 두 곳에서 설치를 시도한다 — 이 중간 스트림 체크와 마지막 볼륨 이후의 꼬리 체크 — 둘 다 동일한 CAS를 사용한다. 꼬리 체크는 file_sync_helper_can_flush && dwb_Global.file_sync_helper_block == NULL && block->count_flush_volumes_info > 0이고 헬퍼가 가용한 경우 발화하며, 위 루프가 한 번도 인계를 트리거하지 않은 케이스를 처리한다.

헬퍼가 가용하지 않을 때는 플러셔가 직접 볼륨을 동기화한다. dwb_flush_block 내부에는 두 개의 별개 인라인 동기화 지점이 있다: (a) 새 블록을 쓰기 전, while (file_sync_helper_block != NULL) 드레인 루프 — 헬퍼가 미가용이면 이전에 파킹된 블록의 볼륨을 fsync하고 ATOMIC_TAS_ADDR로 슬롯을 NULL로 클리어; (b) dwb_write_block 반환 후, NOT_FLUSHED → FLUSH_THREAD를 CAS하고 헬퍼가 클레임하지 않은 각 볼륨을 fsync하는 볼륨별 루프. 생산자의 동작은 전적으로 dwb_is_file_sync_helper_daemon_available() 결과에 달려 있다: 가용 → 게시하고 깨우기; 미가용 → 인라인 fsync. 정합성은 동일하며 지연 시간만 다르다. 6장은 해당 루프들과 세 번째 위임 지점을 해부한다: dwb_write_block 이후의 볼륨별 루프에서 num_pages > PRM_ID_PB_SYNC_ON_NFLUSH / 2인 볼륨은 헬퍼가 가용할 때 헬퍼에게 남겨지며(슬롯이 이미 NULL이 아님을 assert), 6장에서 다룬다.

라이터-투-플러셔 인계 역시 호출이 아닌 깨우기임을 주목하라: 블록이 꽉 차면 라이터 스레드는(dwb_acquire_next_slot의 블록-풀 분기로 이어지는 경로에서) dwb_is_flush_block_daemon_available()이면 dwb_flush_block_daemon을 깨우고 반환한다; 해당 주석은 “이전 블록 대기” 명시적 코드가 제거된 것을 기록하는데, 플러시 데몬이 이미 채워진 순서를 강제하기 때문이다. 플러시 데몬이 미가용일 때만 라이터가 자신의 스레드에서 dwb_flush_block(..., false, NULL)을 인라인으로 호출한다.

7.6 가용성 및 실행 중 판별 조건

섹션 제목: “7.6 가용성 및 실행 중 판별 조건”

#if defined (SERVER_MODE) 아래에서 동일한 형태로 정의된 네 개의 boolean 헬퍼가 있으며, 그 외에서는 false를 반환한다(독립 실행형이 항상 인라인 플러시를 하는 이유). available 쌍(“이 작업을 위임해야 하는가?”)은 PRM_ID_ENABLE_DWB_FLUSH_THREAD == true && dwb_flush_block_daemon != NULL을 반환하고, running 쌍은 여기에 && dwb_flush_block_daemon->is_running()을 추가한다 — “데몬이 현재 본체 내부에 있는가?”. 파일-동기화-헬퍼 변형은 dwb_file_sync_helper_daemon에 같은 구조로 동작한다.

이 구분은 구조 변경(10장)에서 핵심적이다: 변경자는 MODIFYING을 세트한 뒤 ATOMIC_INC_32 (&dwb_Global.blocks_flush_counter, 0) > 0 || dwb_flush_block_daemon_is_running() || dwb_file_sync_helper_daemon_is_running()이 클리어될 때까지 thread_sleep(20)으로 스핀한다. *_available만으로는 부족하다 — 데몬이 가용한 상태이면서 플러시 중일 수 있기 때문이다; 변경자는 running이 클리어되어야만 단독 접근자가 될 수 있다. 데몬들의 틱별 position_with_flags 재독이 MODIFYING에서 탈출하게 하므로, 루프는 종료된다.

  1. 두 데몬이 하나의 파이프라인을 분담한다. "dwb-flush-block"(1 ms)은 블록 하나의 전체 쓰기-동기화를 수행하고, "dwb-file-sync"(10 ms)는 홈 볼륨별 fileio_synchronize 호출을 분리한다. 헬퍼의 함수명 접두사는 dwb_file_sync_helper_*이지만 등록된 데몬 이름"dwb-file-sync"이다.
  2. 인계는 단일 포인터다. dwb_Global.file_sync_helper_block은 블록을 최대 하나만 보유하며, 생산자는 ATOMIC_CAS_ADDR(NULL → block)으로 설치하고 헬퍼는 ATOMIC_TAS_ADDR(→ NULL)로 클리어한다. 단일 슬롯이 블록을 직렬화한다.
  3. 볼륨별 소유권은 3-상태 열거형에 대한 CAS다. flushed_status(FLUSH_VOLUME_STATUS, flush_volume_info의 필드)는 NOT_FLUSHED → FILE_SYNC_HELPER 또는 → FLUSH_THREAD로 전이되며, CAS가 각 홈 볼륨을 정확히 한 스레드가 fsync하도록 보장하고, assert_release가 패배 상태와 num_pages2 >= num_pages 단조성을 고정한다.
  4. 블록은 채워진 순서대로 엄격하게 플러시된다. dwb_get_next_block_for_flushnext_block_to_flush만 제공하며, 완전히 가득 찼을 때만 제공한다; 커서는 dwb_flush_block 내부의 CAS로 전진한다 — 복구 정합성의 불변 조건이다.
  5. 데몬 경로는 최선 노력이며 인라인 폴백이 있다. dwb_is_*_daemon_available()이 위임 여부를 결정하며, 미가용 시 dwb_flush_block이 파킹된 볼륨을 인라인으로 fsync한다(두 지점: 쓰기 전 드레인 루프와 쓰기 후 볼륨별 루프). 정합성은 동일하며 지연 시간만 다르다.
  6. 두 게이트와 부트 플래그가 모든 경로를 보호한다. PRM_ID_ENABLE_DWB_FLUSH_THREAD(선택적 활성화)와 BO_IS_FLUSH_DAEMON_AVAILABLE()이 모두 충족되어야 하며, 독립 실행형 빌드는 *_availablefalse로 단락한다.
  7. availablerunning. 구조 변경은 dwb_*_daemon_is_running() — 포인터가 세트되어 있고 is_running() — 을 기다리므로, 변경자는 단독 접근자가 된 뒤에야 작업을 시작한다.

Chapter 8: 페이지 버퍼 외부에서 프로듀서가 DWB를 건드리는 경우와 강제 드레인

섹션 제목: “Chapter 8: 페이지 버퍼 외부에서 프로듀서가 DWB를 건드리는 경우와 강제 드레인”

3~7장은 DWB 내부를 추적했다. 이 장은 외부 접점 — 엔진이 살아 있는 DWB를 건드리는 세 가지 경로를 다룬다: 읽기 측(pgbuf_fix 미스 시 슬롯에서 더 새로운 사본을 찾는 경우), 비-pgbuf 프로듀서(file_io.c의 직접 쓰기가 DWB를 경유해야 하는 경우), 그리고 강제 드레인. 슬롯 해시를 라이트-백 캐시로 활용하는 개요는 cubrid-double-write-buffer.md의 “Slot hash — turning the DWB into a read-side cache” 절을 참고하라. 여기서는 dwb_read_page, fileio_write_or_add_to_dwb, dwb_flush_force, dwb_synchronize, 그리고 pgbuf_bcb_safe_flush_internalpgbuf_bcb_flush_with_wal 체인의 모든 분기를 추적한다.

pgbuf_fix의 미스 경로에서 fileio_read 호출 이전에, page_buffer.c의 콜 사이트는 세 갈래 if/else if/else if 안에서 dwb_read_page를 호출한다. 첫 번째 갈래(NO_ERROR가 아닌 경우)는 assert(false)를 실행한 뒤 아무것도 해제하지 않고 return NULL한다 — 실제로 dwb_read_page는 오류를 반환하지 않으며 항상 NO_ERROR를 돌려준다. 두 번째 갈래(success == true)는 디스크 읽기를 건너뛴다 — 슬롯에서 바로 복사한 것이다. 세 번째 갈래가 fileio_read를 실행하며, 읽기 오류가 발생할 경우 이 갈래에서만 BCB가 롤백된다(pgbuf_put_bcb_into_invalid_list + pgbuf_unlock_page). 이것이 읽기 계약의 전부다. dwb_read_page 자체는 짧지만 각 분기 하나하나가 중요하다:

// dwb_read_page -- src/storage/double_write_buffer.cpp (body)
*success = false; /* <- default: caller will fileio_read */
if (!dwb_is_created ()) return NO_ERROR; /* (A) DWB off -> always a miss */
VPID key_vpid = *vpid; /* find() takes a non-const VPID& */
slots_hash_entry = dwb_Global.slots_hashmap.find (thread_p, key_vpid);
if (slots_hash_entry != NULL) /* (B) some slot hashed under this VPID */
{ /* assert (slots_hash_entry->slot->io_page != NULL); */
if (VPID_EQ (&slots_hash_entry->slot->vpid, vpid)) /* (C) re-check under entry mutex */
{ memcpy (io_page, slots_hash_entry->slot->io_page, IO_PAGESIZE);
/* assert prv.pageid/prv.volid still match vpid -- second invariant layer */
*success = true; } /* <- genuine hit */
pthread_mutex_unlock (&slots_hash_entry->mutex); } /* (D) find() left this locked */
return NO_ERROR;

(A)와 (B)는 직관적이다(Figure 8-1). findper-entry mutex를 보유한 채 엔트리를 반환하므로, (D)는 (C)의 두 하위 분기 모두에서 잠금을 해제한다. (C)의 VPID_EQ 재확인이 미묘한 부분이다: find 일치 후 mutex를 획득하기 전 사이에 엔트리의 slot 포인터가 다른 VPID를 가진 슬롯으로 교체될 수 있다(dwb_slots_hash_insert는 5장에서 설명한 대로 엔트리를 재사용한다). 따라서 memcpy 이전에 다시 검증한다. memcpy 직후의 assert는 복사된 바이트의 prv.pageid/prv.volid를 요청된 VPID와 비교하는 디버그 빌드 전용 두 번째 불변식 검사 계층이다.

불변식 — DWB 읽기 히트는 자기 일관성(self-consistent) 있는 페이지 이미지를 반환한다. memcpy는 entry mutex를 보유한 상태에서, 그리고 VPID_EQ 통과 후에만 실행된다. 프로듀서도 dwb_slots_hash_insert에서 동일한 mutex를 취득해야 한다. 재확인을 생략하면 읽기 측이 바이트가 이미 무관한 다른 페이지의 것으로 바뀐 슬롯을 복사하게 된다.

flowchart TB
  A["dwb_read_page(vpid)"]
  B{"dwb_is_created?"}
  C["반환: *success=false\n(호출자가 fileio_read 실행)"]
  D["slots_hashmap.find(vpid)\nentry 반환 + entry mutex 보유"]
  E{"entry != NULL?"}
  F{"VPID_EQ(slot->vpid, vpid)?"}
  G["memcpy slot->io_page -> 호출자 buf\n*success=true"]
  H["*success=false 유지\n(슬롯이 덮어써짐)"]
  I["pthread_mutex_unlock(entry->mutex)"]

  A --> B
  B -- "no" --> C
  B -- "yes" --> D --> E
  E -- "no (미스)" --> C
  E -- "yes" --> F
  F -- "yes (히트)" --> G --> I
  F -- "no (경쟁 발생)" --> H --> I

Figure 8-1 — dwb_read_page 분기 맵. VPID_EQ가 확인된 경로만 *success를 설정하며, 나머지 모든 경로는 홈 볼륨 읽기로 폴백한다.

dwb_read_page가 슬롯을 찾을 수 있는 것은 dwb_add_page가 해시에 등록한 이후뿐이다. 프로듀서는 먼저 스테이징(dwb_set_data_on_next_slot — 바이트는 복사됐으나 아직 해시 미등록 = STAGED)하고, WAL을 강제 플러시한 뒤, 공개(dwb_add_page = HASHED, §8.3)한다. STAGED와 HASHED 사이 구간에서는 slots_hashmap.find가 NULL을 반환하므로 동시 독자는 DWB를 미스하고 이전 홈 이미지를 읽게 된다.

불변식 — STAGED 구간에서 오래된 홈 이미지를 읽는 것은 안전하다. 새 이미지는 아직 내구적이지 않다(구간 시작 시점에는 WAL조차 강제되지 않은 상태다). 따라서 오래된 읽기는 직전 커밋된 이미지를 반환하며 — 이는 올바른 값이다. 해시는 가속기이지 정합성의 관문이 아니다. STAGED-but-unhashed 슬롯을 들여다보면 WAL이 아직 디스크에 없는 페이지가 노출된다.

8.3 페이지 버퍼 프로듀서 — pgbuf_bcb_safe_flush_internal에서 pgbuf_bcb_flush_with_wal까지

섹션 제목: “8.3 페이지 버퍼 프로듀서 — pgbuf_bcb_safe_flush_internal에서 pgbuf_bcb_flush_with_wal까지”

지배적인 프로듀서는 페이지 버퍼 플러시다. pgbuf_bcb_safe_flush_internal!pgbuf_bcb_is_dirty이면 조기 반환하고, 이후 CAS 루프가 BCB를 세 가지 결과로 분류한다. immediate_flush만이 DWB에 도달한다:

결과조건동작
not dirty!pgbuf_bcb_is_dirtyreturn NO_ERROR — DWB를 전혀 건드리지 않음
immediate_flush플러시 중 아님 AND (unlatched / read-latched / self write-latched)pgbuf_bcb_flush_with_wal (프로듀서 본체)
deferred다른 스레드가 write-latch 보유 중이거나 이미 플러시 중PGBUF_BCB_ASYNC_FLUSH_REQ 설정, 또는 synchronous이면 대기

pgbuf_bcb_flush_with_wal 내부에서 DWB 게이트는 한 번 계산된다:

// pgbuf_bcb_flush_with_wal -- src/storage/page_buffer.c
uses_dwb = dwb_is_created () && !is_temp; /* <- temp volumes never use the DWB */
start_copy_page:
/* iopage = aligned scratch; tde_encrypt_data_page(...) OR memcpy(iopage, &bcb->iopage) */
if (uses_dwb)
{ error = dwb_set_data_on_next_slot (thread_p, iopage, false, false, &dwb_slot); /* (a) STAGE */
if (error != NO_ERROR) return error;
if (dwb_slot != NULL) { iopage = NULL; goto copy_unflushed_lsa; } } /* else fall through */
copy_unflushed_lsa:
/* ... unlock BCB, capture lsa / oldest_unflush_lsa ... */
if (!LSA_ISNULL (&oldest_unflush_lsa))
logpb_flush_log_for_wal (thread_p, &lsa); /* (b) WAL FORCE */
if (uses_dwb)
{ error = dwb_add_page (thread_p, iopage, &bufptr->vpid, false, &dwb_slot); /* (c) PUBLISH */
if (error == NO_ERROR && dwb_slot == NULL)
{ uses_dwb = false; PGBUF_BCB_LOCK (bufptr); goto start_copy_page; } } /* (d) */
else /* (e) non-DWB arm: fileio_write with write_mode chosen per §8.4 */
fileio_write (thread_p, fileio_get_volume_descriptor (bufptr->vpid.volid),
iopage, bufptr->vpid.pageid, IO_PAGESIZE, write_mode);

직관적이지 않은 분기가 두 개 있다. (b) WAL FORCE는 oldest_unflush_lsa가 non-NULL일 때만 실행된다 — NULL이면 건너뛴다(로그 없이 더티해진 페이지이며, 이는 허용된다). (d)에서 dwb_add_page(혹은 (a))가 dwb_slot == NULL을 반환하면 DWB가 플러시 도중 해체된 것이다 → BCB를 재잠금하고 비-DWB 경로로 복사를 재시도한다.

불변식 — WAL은 STAGE와 PUBLISH 사이에서 강제되며, 그 이후에는 실행되지 않는다. dwb_set_data_on_next_slotdwb_add_page 사이에 logpb_flush_log_for_wal이 삽입되어 있어, 슬롯이 독자에게 공개(§8.1)되는 시점(PUBLISH)에는 해당 WAL이 이미 디스크에 있음이 보장된다. 순서가 뒤집히면 크래시 시 DWB의 온-디스크 사본이 로그보다 앞설 수 있다.

8.4 FILEIO_WRITE_NO_COMPENSATE_WRITE 노브

섹션 제목: “8.4 FILEIO_WRITE_NO_COMPENSATE_WRITE 노브”

두 프로듀서 모두 dwb_is_created()fileio_write 모드를 결정한다. 이는 “DWB가 현재 torn-page 방어 메커니즘으로 활성화되어 있는가”를 인코딩한다: DWB가 살아 있으면 FILEIO_WRITE_NO_COMPENSATE_WRITE(DWB가 torn-page 복구를 담당하므로 fileio_write는 자체적인 partial-write 보상을 건너뜀), 그렇지 않으면 FILEIO_WRITE_DEFAULT_WRITE(fileio_write가 유일한 torn-page 방어 수단이므로 partial write를 재발행함). §8.5의 (7)과 요약 항목 5에서 참조된다.

8.5 비-pgbuf 프로듀서 — fileio_write_or_add_to_dwb

섹션 제목: “8.5 비-pgbuf 프로듀서 — fileio_write_or_add_to_dwb”

포매팅, 페이지 배열 초기화, 볼륨 간 복사 같은 file_io.c 직접 쓰기는 fileio_write_or_add_to_dwb를 거쳐 처리된다. DWB가 활성 상태라면 홈에 직접 쓰는 대신 스테이징한다:

// fileio_write_or_add_to_dwb -- src/storage/file_io.c (SERVER_MODE arm; CS_MODE = plain write)
skip_flush = dwb_is_created (); /* (1) is the DWB the active mechanism? */
if (skip_flush)
{
arg.vdes = vol_fd;
vol_info_p = fileio_traverse_permanent_volume (thread_p,
fileio_is_volume_descriptor_equal, &arg); /* (2) permanent volume? */
if (vol_info_p)
{
VPID_SET (&vpid, vol_info_p->volid, page_id);
io_page_p->prv.volid = vol_info_p->volid; /* stamp header so slot/hash */
io_page_p->prv.pageid = page_id; /* key stays consistent */
error_code = dwb_add_page (thread_p, io_page_p, &vpid, ensure_metadata, &p_dwb_slot);
if (error_code != NO_ERROR) return NULL; /* (3) hard error */
else if (p_dwb_slot != NULL) return io_page_p;/* (4) staged -> done */
/* (5) p_dwb_slot==NULL: DWB disabled, fall through */
}
/* (6) not permanent volume: fall through */
}
write_mode = skip_flush ? FILEIO_WRITE_NO_COMPENSATE_WRITE : FILEIO_WRITE_DEFAULT_WRITE; /* (7) §8.4 */
return fileio_write (thread_p, vol_fd, io_page_p, page_id, page_size, write_mode);

페이지 버퍼 프로듀서와 달리 STAGE 단계가 없고(호출자가 io_page_p를 소유하며 dwb_add_page에 직접 전달), WAL 강제도 없다(비로그 구조적 쓰기이기 때문). 인라인 태그가 분기를 설명한다: (2)는 §8.3의 is_temp 게이트를 반영하고, (5)/(6) — DWB 비활성 또는 비-permanent 볼륨 — 은 (7)의 직접 쓰기 폴백을 공유한다. CS_MODE에서는 이 함수가 단순한 fileio_write로 동작한다.

체크포인트, sync, 또는 dismount는 스테이징된 모든 내용이 먼저 내구적인 홈 스토리지에 도달했음을 보장해야 한다. “모든 블록을 플러시”하는 방식으로는 충분하지 않다(프로듀서가 블록을 채우는 도중일 수 있다). 대신 아직 플러시되지 않은 블록 중 가장 최신 것을 선택하고, 프로듀서가 멈춰 있으면 null 페이지로 패딩하면서 완료까지 밀어붙인다 — position_with_flags를 상태로 하는 goto 상태 머신이다:

// dwb_flush_force -- src/storage/double_write_buffer.cpp (goto graph; tags keyed to prose)
start: // (A) !DWB_IS_CREATED->end; (B) MODIFYING->wait; (C) status==0,no helper->end / (D)->tail
// (E) pick highest-version DWB_IS_BLOCK_WRITE_STARTED block ; (F) position moved -> start
check_flushed_blocks:
// (G) daemon flushing this full block -> dwb_wait_for_block_completion ; retry
// (H) not write-started / (I) version bumped -> goto tail
if (current_position_with_flags == prev_position_with_flags && count_added_pages < max_pages_to_add)
{ dwb_add_page (thread_p, iopage, &null_vpid, false, &dwb_slot); // (J) pad with null page
if (dwb_slot == NULL) goto end; count_added_pages++; } // (K) DWB disabled -> done
prev_position_with_flags = current_position_with_flags; goto check_flushed_blocks;
wait_for_file_sync_helper_block: // tail
while (dwb_Global.file_sync_helper_block == &dwb_Global.blocks[initial_block_no])
thread_sleep (1); // (L) wait out helper fsync
end:
*all_sync = true; return NO_ERROR; // (M) caller may skip fsync

인라인 태그 이외에 주목할 점: (E) 버전이 가장 높은 started 블록이 가장 최신의 미플러시 블록이다(이전 블록은 플러시 순서에 따라 이미 제거됐다). 따라서 이 블록을 드레인하는 것으로 충분하다. **(D)/(H)/(I)**는 tail 전용이다 — 해당 블록이 이미 플러시됐거나 새 버전으로 재사용됐으며, helper의 홈 fsync만 남은 상태다. (J)/(K) 패딩이 전진 보장 엔진이다:

불변식 — dwb_flush_force는 활성 프로듀서 없이도 전진한다. 반쯤 찬 블록이 채워지기를 기다리면, 강제자가 유일한 DWB 스레드일 경우 교착 상태에 빠진다. (J) 패딩은 DWB_BLOCK_NUM_PAGES에 도달할 페이지를 직접 만들어내어 플러시를 스스로 촉발한다.

flowchart LR
  S["스냅샷"] -- "DWB 꺼짐" --> D["완료\nall_sync=true"]
  S -- "helper 사용 중" --> T["HelperTail\nhelper 대기"]
  S -- "블록 시작됨" --> P["블록 선택"] --> C{"플러시 확인"}
  C -- "유휴, 공간 있음" --> J["null 페이지 패딩"] --> C
  C -- "사라짐 / 버전 증가" --> T --> D

Figure 8-2 — dwb_flush_force 흐름. Pad 자기 루프가 전진 보장 엔진이다. (F)/(G)의 재시작/대기 경로는 생략.

8.7 볼륨별 드레인 — dwb_synchronize

섹션 제목: “8.7 볼륨별 드레인 — dwb_synchronize”

dwb_synchronize는 단일 볼륨 fsync를 래핑하되, 먼저 대기 중인 DWB 내용을 밀어내야 한다(해당 볼륨의 최신 페이지가 아직 슬롯에 있을 수 있으므로):

// dwb_synchronize -- src/storage/double_write_buffer.cpp
bool complete = false;
if (fileio_fsync_pending ())
return vol_fd; /* (1) fsync suppressed this round -> no-op */
if (fileio_is_permanent_volume_descriptor (thread_p, vol_fd)) /* CS_MODE-guarded */
error = dwb_flush_force (thread_p, &complete); /* (2) drain the DWB */
if (error == NO_ERROR && complete == false)
error = fsync (vol_fd); /* (3) DWB didn't cover it -> fsync directly */
if (error != NO_ERROR) /* (4) fatal: ER_IO_SYNC, return NULL_VOLDES */
{ er_set_with_oserror (...); return NULL_VOLDES; }
perfmon_inc_stat (thread_p, PSTAT_FILE_NUM_IOSYNCHES); return vol_fd;

직관적이지 않은 분기는 **(1)**이다: PRM_ID_SUPPRESS_FSYNC가 켜져 있으면 fileio_fsync_pending은 N번마다 한 번을 제외하고 true를 반환하므로, dwb_synchronize는 아무것도 건드리지 않고 반환한다(fsync 일괄처리 밸브). **(2)**는 permanent 볼륨에 한해서만 드레인한다(complete는 §8.6의 all_sync에 해당한다). (3) 직접 fsynccomplete == false일 때만 실행되어 이중 fsync를 방지한다.

불변식 — dwb_synchronize는 DWB 커버 볼륨을 이중으로 fsync하지 않는다. (3)의 complete == false 가드는 드레인이 홈 볼륨을 이미 sync했을 때는 명시적 fsync(vol_fd)를 실행하지 않는다. (3)이 무조건 실행된다면 DWB 커버 볼륨마다 요청당 두 번씩 fsync하는 셈이 된다.

8.8 콜 사이트 — 누가 DWB를 드레인하는가

섹션 제목: “8.8 콜 사이트 — 누가 DWB를 드레인하는가”

두 기본 연산은 file_io.c 내에서 서로 다른 단위로 연결된다. 주의: fileio_synchronize 자체는 DWB를 드레인하지 않으며 직접 fsync한다. 드레인은 아래의 볼륨 연산 호출자에 위치한다.

호출자단위기본 연산목적
fileio_synchronize_all전체 데이터베이스dwb_flush_forceDWB 드레인 후 !all_sync일 때만 볼륨 sweep
fileio_reset_volume볼륨 하나dwb_synchronize페이지 LSA 초기화: 드레인 후 fsync
fileio_copy_volume볼륨 하나dwb_synchronizefileio_write_or_add_to_dwb로 복사 후 내구성 선언 전 드레인
fileio_dismount볼륨 하나dwb_synchronize디스크립터 닫기 전 드레인
fileio_dismount_volume볼륨 하나dwb_synchronizedismount-all 중 볼륨별 sweep

fileio_synchronize_alldwb_flush_force(thread_p, &all_sync)를 호출한 후, all_sync == false일 때만 볼륨별 sweep를 수행한다. true이면 DWB 자체의 홈 볼륨 fsync(6장)가 모든 permanent 볼륨을 이미 커버한 것이다 — §8.7의 complete 단락을 전체 데이터베이스 단위로 구현한 방식이다. 참고 자료에는 fileio_synchronize_volume_and_dwb가 드레인 래퍼로 언급되지만, 해당 함수는 존재하지 않는다 — 드레인은 위 네 호출자가 각자 인라인으로 처리한다(이 드리프트는 9장에서 기록한다).

file_io.c 외부에도 두 개의 드레인 콜 사이트가 존재한다. disk_manager.cdwb_flush_force 하나와 dwb_synchronize 하나가 있으며, 둘 다 볼륨 포맷/확장 경로에 있다. 위 표와 동일한 계약을 따른다. 표의 범위를 file_io.c로 한정한 이유는 볼륨 내구성이 주로 그곳에서 구동되기 때문이다.

  1. dwb_read_page는 읽기 측 라이트-백 캐시로, 콜 사이트의 세 갈래 if/else if/else if에서 fileio_read 이전에 조회된다.
  2. VPID_EQ 재확인이 정합성 보장 장치다 — 재사용 가능한 슬롯이 여전히 일치하는지 entry mutex 하에서 확인한 후에만 복사를 수행한다.
  3. STAGED-but-not-yet-HASHED 구간은 무해하다(§8.2): 미스 시 이전 홈 이미지를 읽으며, 새 WAL이 아직 내구적이지 않으므로 이는 올바른 값이다.
  4. 페이지 버퍼 프로듀서는 STAGE / WAL-강제 / PUBLISH를 분리한다. DWB 호출 중 하나에서 dwb_slot == NULL이 반환되면 비-DWB 폴백이 트리거된다.
  5. fileio_write_or_add_to_dwb는 비-pgbuf 프로듀서다(STAGE 없음, WAL 강제 없음). 두 프로듀서 모두 §8.4에 따라 쓰기 모드를 결정한다.
  6. dwb_flush_force는 패딩으로 드레인한다 — 최신 블록이 채워져 플러시될 때까지 null 페이지를 채우고, helper의 tail을 대기한다.
  7. 드레인 콜 사이트는 이중 fsync를 피한다complete/all_sync 가드와 fileio_fsync_pending 스로틀로. fileio_synchronize 자체는 DWB를 드레인하지 않는다.

Chapter 9: 크래시 복구와 볼륨 재생

섹션 제목: “Chapter 9: 크래시 복구와 볼륨 재생”

이 챕터는 하나의 질문에 답한다: 재시작 시, 디스크 상의 DWB 볼륨을 어떻게 읽어 들이고, torn home page를 복구하며, 새 볼륨을 다시 만드는가 — 그것도 로그 redo가 실행되기 전에? 상위 레벨 문서 (cubrid-double-write-buffer.md)는 torn-page 구간이 왜 존재하는지를 설명한다; 전체 재시작 파이프라인에서 이 단계의 위치는 cubrid-recovery-manager-detail.md (pre-redo 단계)를 참고한다. 진입점은 dwb_load_and_recover_pages이며, 부트 시퀀스에서 정확히 한 번 호출되고, 복구 완료 후 다음 보장을 남긴다:

불변 조건 9-A (복구 인계). dwb_load_and_recover_pagesNO_ERROR를 반환할 때, 디스크 상의 구 DWB 볼륨은 더 이상 존재하지 않고, 설정된 크기의 새 빈 DWB가 생성되어 있으며, 크래시 시점에 torn(절반만 쓰인) 상태였던 모든 home page는 마지막으로 완전히 플러시된 — 체크섬이 유효한 — DWB 이미지로 덮어써진 상태다. 따라서 로그 redo는 일관된(torn 되지 않은) 페이지 위에서 재생된다.

9.1 부트 시퀀스 내에서의 복구 위치

섹션 제목: “9.1 부트 시퀀스 내에서의 복구 위치”

dwb_load_and_recover_pagesboot_sr.cboot_restart_server 재시작 경로에 연결되어 있으며, vacuum_initializelog_initialize 사이에 위치한다:

// boot_restart_server (restart path) -- src/transaction/boot_sr.c
vacuum_initialize (thread_p, ...); /* vacuum data loaded before recovery */
oid_set_root (&boot_Db_parm->rootclass_oid);
/* Load and recover data pages before log recovery */
error_code = dwb_load_and_recover_pages (thread_p, log_path, log_prefix);
// ... ASSERT_ERROR + goto error on failure ...
#if defined(SERVER_MODE)
pgbuf_daemons_init ();
dwb_daemons_init (); /* DWB flush daemons start AFTER recovery */
parallel_query::worker_manager_global::get_manager ().init ();
#endif
log_initialize (thread_p, ...); /* -> log_recovery (analysis + redo) */

이 순서는 세 가지 측면에서 필수적이다: dwb_daemons_init 이전 (복구가 DWB를 단독으로 점유 — 동시성 없음, 구조 변경 댄스 없음, Chapter 10 참고); log_initialize / log_recovery 이전 (redo가 torn 되지 않은 home을 받음); vacuum_initialize 이후 (복구가 쓰기를 통과시키는 데이터 볼륨 디스크립터가 이미 마운트된 상태). 빈 DWB + 일관된 페이지 상태(불변 조건 9-A) 가 redo에 인계되는 계약이다.

9.2 dwb_load_and_recover_pages — 분기 완전 해설

섹션 제목: “9.2 dwb_load_and_recover_pages — 분기 완전 해설”

진입 시 dwb_Global.vdes는 반드시 NULL_VOLDES여야 한다 — 전역 DWB가 아직 연결되어 있지 않다. 이는 처리(handle)되는 것이 아니라 단언(assert)된다: 비-null vdes는 부트 순서 버그다.

// dwb_load_and_recover_pages -- src/storage/double_write_buffer.cpp
assert (dwb_Global.vdes == NULL_VOLDES); /* <- DWB not attached yet */
fileio_make_dwb_name (dwb_Volume_name, dwb_path_p, db_name_p);
if (fileio_is_volume_exist (dwb_Volume_name)) /* <- Branch A: volume present */
{
read_fd = fileio_mount (..., dwb_Volume_name, LOG_DBDWB_VOLID, false, false);
if (read_fd == NULL_VOLDES)
return ER_IO_MOUNT_FAIL; /* <- early return, no cleanup */
num_dwb_pages = fileio_get_number_of_volume_pages (read_fd, IO_PAGESIZE);
if ((num_dwb_pages > 0) && IS_POWER_OF_2 (num_dwb_pages)) /* <- Branch B: sane size */
{ /* ... read, order, dedup, sanity, repair ... */ }
fileio_dismount (thread_p, read_fd); /* always reached in Branch A */
fileio_unformat (thread_p, dwb_Volume_name); /* <- destroy old DWB */
read_fd = NULL_VOLDES;
}
error_code = dwb_create (thread_p, dwb_path_p, db_name_p); /* <- fresh DWB, always */

외부 if와 내부 2의 거듭제곱 검사가 모든 재시작 형태를 다룬다(분류는 §9.10 항목 2 참고). DWB 볼륨 부재 경우(Branch A false) — 정상 종료 시 삭제됐거나 최초 부트인 경우 — 는 곧바로 dwb_create로 진행된다: 복구할 것이 없다. Branch B-false가 더 중요하다: 소스 주석에 따르면 유효한 DWB 크기는 반드시 2의 거듭제곱이어야 하므로, 0이거나 2의 거듭제곱이 아닌 카운트는 볼륨이 생성 됐지만 일관되게 플러시된 적이 없음을 의미한다(생성 도중 크래시) — 복구를 건너뛰고, home에는 아무것도 쓰지 않으며, 제어 흐름은 여전히 dismount/unformat/재생성으로 진행된다.

원래 블록 수에 상관없이 단일 복구 블록. 복구는 dwb_create_blocks (thread_p, 1, num_dwb_pages, &rcv_block)을 호출한다: Chapter 3의 설정된 블록당 크기가 아니라, 전체 디스크 상의 DWB 크기에 맞춘 하나의 DWB_BLOCK — 복구는 단일 fileio_read_pages로 채울 수 있도록 모든 페이지가 연속적으로 배치되기만 하면 된다.

공유 end: 레이블(모든 에러 goto와 성공 경로)은 p_dwb_ordered_slots를 해제하고, rcv_block을 정리 및 해제한 뒤 error_code를 반환한다. 에러 시 구 DWB 볼륨은 의도적으로 디스크에 남겨진다(소스 주석: “Do not remove the old file if an error occurs”) — 반쯤 파괴된 DWB는 유일한 좋은 복사본을 잃게 되므로, 운영자가 재시도할 수 있도록 한다. fileio_unformat은 Branch A 성공 경로에서만 실행된다.

Branch B로 진입하면, 전체 DWB 볼륨이 복구 블록의 write_buffer에 한 번에 읽혀지고, 각 슬롯의 신원은 io_page->prv — 프로듀서가 페이지를 스테이징할 때 찍은 volid/pageid/LSA 트리플(Chapter 4) — 로부터 재구성된다:

// dwb_load_and_recover_pages (slot rederivation) -- src/storage/double_write_buffer.cpp
if (fileio_read_pages (thread_p, read_fd, rcv_block->write_buffer, 0,
num_dwb_pages, IO_PAGESIZE) == NULL)
{ error_code = ER_FAILED; goto end; }
for (i = 0; i < num_dwb_pages; i++)
{
iopage = rcv_block->slots[i].io_page; /* slot.io_page aliases write_buffer */
VPID_SET (&rcv_block->slots[i].vpid, iopage->prv.volid, iopage->prv.pageid);
LSA_COPY (&rcv_block->slots[i].lsa, &iopage->prv.lsa);
}
rcv_block->count_wb_pages = num_dwb_pages; /* every page counts at recovery */

home VPID가 페이지 이미지 내부에 있으므로, 재구성은 단순한 필드 복사다 — 디스크 상의 외부 슬롯 인덱스는 없다. count_wb_pages는 전체 페이지 수로 설정되어 모든 슬롯이 집계된다.

9.4 dwb_block_create_ordered_slots — (VPID, LSA) 기준 정렬

섹션 제목: “9.4 dwb_block_create_ordered_slots — (VPID, LSA) 기준 정렬”

효율적인 복구와 중복 탐지를 위해, 슬롯을 임시 배열에 복사한 뒤 정렬한다:

// dwb_block_create_ordered_slots -- src/storage/double_write_buffer.cpp
p_local_dwb_ordered_slots =
(DWB_SLOT *) malloc ((block->count_wb_pages + 1) * sizeof (DWB_SLOT)); /* +1 sentinel */
// ... ER_OUT_OF_VIRTUAL_MEMORY on NULL ...
memcpy (p_local_dwb_ordered_slots, block->slots, block->count_wb_pages * sizeof (DWB_SLOT));
dwb_init_slot (&p_local_dwb_ordered_slots[block->count_wb_pages]); /* sentinel at [count] */
qsort (p_local_dwb_ordered_slots, block->count_wb_pages, sizeof (DWB_SLOT), dwb_compare_slots);
*p_dwb_ordered_slots = p_local_dwb_ordered_slots;
*p_ordered_slots_length = block->count_wb_pages + 1; /* includes sentinel */

dwb_compare_slotsvpid.volid, vpid.pageid, lsa.pageid, lsa.offset 순으로 사전식 정렬을 수행한다(LSA 필드는 오버플로 방지를 위해 64비트 INT64 차이, VPID 필드는 int 차이 사용). 따라서 동일한 home VPID를 가진 두 슬롯은 인접하게 놓이며, LSA가 작은(오래된) 것이 먼저 온다.

불변 조건 9-B (정렬된 슬롯 인접성). dwb_block_create_ordered_slots 실행 후, 중복 home page들은 연속되어 있고 LSA 오름차순이다. dedup 루프와 디버그 체커가 이에 의존하며, 항상 이웃 i-1/i만 비교한다.

센티넬은 정렬 영역에 포함되지 않는다. qsort는 첫 count_wb_pages 원소만 정렬하며, 센티넬은 인덱스 count_wb_pages — 정렬 범위 — 에 위치하고, 비교자에 절대 전달되지 않는다(null-VPID 슬롯은 비교된다면 맨 앞으로 정렬되겠 지만, 실제로는 그렇지 않다). 센티넬은 오직 dwb_write_blockordered_slots_length = count_wb_pages + 1 루프가 null 슬롯에서 종료될 수 있도록 하기 위해 존재한다; dedup 및 sanity 루프는 count_wb_pages에서 경계가 지어지며 센티넬에 닿지 않는다.

같은 페이지가 두 번 나타나는 것은 플러시 도중 크래시가 발생했을 때 일어난다: 일부 슬롯은 현재 플러시를, 다른 슬롯은 이전 플러시를 담고 있다. dedup 루프는 낡은 복사본을 무효화하여 좋은 복사본을 덮어쓰는 일이 없게 한다.

// dwb_load_and_recover_pages (dedup) -- src/storage/double_write_buffer.cpp
for (i = 0; i < rcv_block->count_wb_pages - 1; i++)
{
DWB_SLOT *s1 = &p_dwb_ordered_slots[i], *s2 = &p_dwb_ordered_slots[i + 1];
if (!VPID_ISNULL (&s1->vpid) && VPID_EQ (&s1->vpid, &s2->vpid))
{
assert (LSA_LE (&s1->lsa, &s2->lsa)); /* guaranteed by 9-B sort */
if (LSA_LT (&s1->lsa, &s2->lsa))
VPID_SET_NULL (&s1->vpid); /* older LSA -> invalidate s1 */
else /* equal LSA: fall back to position_in_block */
{
assert (s1->position_in_block != s2->position_in_block);
if (s1->position_in_block < s2->position_in_block)
VPID_SET_NULL (&s2->vpid); /* earlier slot = last flush, keep s1 */
else
VPID_SET_NULL (&s1->vpid); /* keep s2 */
}
}
}

인접한 동일-VPID 쌍에서: s1->lsa < s2->lsa 이면 s1을 null로 만든다. LSA가 같은 경우(LSA를 전진시키지 않은 상태에서 dirty) position_in_block 으로 판단한다 — 프로듀서는 오름차순 위치를 채우므로, 더 낮은 위치가 가장 최근 플러시다: 낮은 것을 유지하고 높은 것을 null로 만든다. VPID를 null로 만드는 것 이 이 챕터에서의 “이 슬롯 무시”: 이후 모든 하위 루프는 VPID_ISNULL을 건너뛴다.

9.6 dwb_debug_check_dwb — 중복 탐지기 (NDEBUG 조건부 컴파일)

섹션 제목: “9.6 dwb_debug_check_dwb — 중복 탐지기 (NDEBUG 조건부 컴파일)”

디버그 빌드에서만 컴파일된다(#if !defined (NDEBUG)). dedup 이후 실제 중복이 하나도 남지 않았는지 확인하는 이중 안전망으로, 이웃 i-1/i를 순회한다:

// dwb_debug_check_dwb -- src/storage/double_write_buffer.cpp
for (i = 1; i < num_dwb_pages; i++)
{
if (VPID_ISNULL (&p_dwb_ordered_slots[i].vpid)) continue; /* deduped/empty */
if (VPID_EQ (&p_dwb_ordered_slots[i].vpid, &p_dwb_ordered_slots[i - 1].vpid))
{
error_code = fileio_page_check_corruption (thread_p, p_dwb_ordered_slots[i - 1].io_page, &is_page_corrupted);
if (error_code != NO_ERROR) return error_code; /* <- check failed -> short-circuit */
if (is_page_corrupted) continue; /* one corrupt -> not a real dup */
// ... same error_code-checked check on slot i; two memcmp vs a zeroed page skip uninit slots ...
assert (false); /* genuine duplicate -> bug */
}
}

fileio_page_check_corruption 호출 모두 error_code를 캡처하고, 실패 시 return error_code로 단락(short-circuit)한다(두 번째는 위에서 축약). 그렇지 않은 경우, 같은 VPID의 이웃은 한쪽이 corrupted이거나, 어느 한쪽이 새로 zeroed 된 iopagememcmp로 동일한(초기화되지 않은 슬롯) 경우에만 허용된다; 정상적이고 구별 가능한 동일-VPID 페이지 쌍이 나타나면 assert (false)를 발동한다 — §9.5가 이미 한쪽을 null로 만들었어야 하기 때문이다. 릴리즈 빌드에서는 이 함수가 제거된다 — 불변 조건 9-B에 대한 개발 단계 수비다.

9.7 dwb_check_data_page_is_sane — 복구 대상 판별

섹션 제목: “9.7 dwb_check_data_page_is_sane — 복구 대상 판별”

복구의 핵심: 살아남은 각 슬롯의 현재 home page를 읽고, 페이지 체크섬을 기준으로 DWB 복사본으로 덮어써야 하는지 판단하며, 복구 가능한 페이지 수를 집계하여 §9.8이 쓸 것이 없을 때 쓰기를 건너뛸 수 있게 한다. 여기서 “torn”은 페이지-체크섬 검사(fileio_page_check_corruption) 실패를 뜻한다 — 크래시 시점에 절반만 쓰인 페이지는 체크섬이 일치하지 않는다.

flowchart TB
  S["슬롯 i"] --> N{"VPID null /\nfd null /\npageid >= vol_pages?"}
  N -- 예 --> SKIP["건너뜀"]
  N -- 아니오 --> R["fileio_read home"]
  R -- "읽기 또는 검사 오류" --> RERR["error_code 반환"]
  R -- ok --> C{"home 체크섬 정상?"}
  C -- "예, 정상" --> CLEAN["슬롯 VPID null +\nfileio_initialize_res"]
  C -- "아니오, 검사 오류" --> RERR
  C -- "아니오, torn" --> D{"DWB 복사본 체크섬 정상?"}
  D -- "아니오, 손상" --> FATAL["assert_release false\nER_FAILED 반환"]
  D -- "예, 정상" --> REC["num_recoverable_pages++"]

Figure 9-1 — dwb_check_data_page_is_sane의 슬롯별 판단 흐름. 모든 분기는 건너뜀, clean-건너뜀, 상위 error_code 반환(fileio_read 실패 또는 두 fileio_page_check_corruption 중 하나 실패), 치명적 ER_FAILED, 복구 중 하나로 끝난다.

// dwb_check_data_page_is_sane -- src/storage/double_write_buffer.cpp
for (i = 0; i < rcv_block->count_wb_pages; i++)
{
vpid = &p_dwb_ordered_slots[i].vpid;
if (VPID_ISNULL (vpid)) continue; /* deduped/cleaned -> skip */
if (volid != vpid->volid) /* volume changed -> refresh fd */
{
temp_vol_fd = fileio_get_volume_descriptor (vpid->volid);
if (temp_vol_fd == NULL_VOLDES) continue; /* not mounted -> skip */
vol_fd = temp_vol_fd; volid = vpid->volid;
vol_pages = fileio_get_number_of_volume_pages (vol_fd, IO_PAGESIZE);
}
if (vpid->pageid >= vol_pages) continue; /* beyond data volume -> skip */
if (fileio_read (thread_p, vol_fd, iopage, vpid->pageid, IO_PAGESIZE) == NULL)
{ ASSERT_ERROR_AND_SET (error_code); return error_code; } /* read error */
error_code = fileio_page_check_corruption (thread_p, iopage, &is_page_corrupted);
if (error_code != NO_ERROR) return error_code; /* <- home check failed */
if (!is_page_corrupted) /* HOME SANE -> never overwrite */
{
VPID_SET_NULL (&p_dwb_ordered_slots[i].vpid);
fileio_initialize_res (thread_p, p_dwb_ordered_slots[i].io_page, IO_PAGESIZE);
continue;
}
/* HOME TORN -> validate the DWB copy's checksum before trusting it */
error_code = fileio_page_check_corruption (thread_p, p_dwb_ordered_slots[i].io_page, &is_page_corrupted);
if (error_code != NO_ERROR) return error_code; /* <- DWB check failed */
if (is_page_corrupted) /* BOTH CORRUPT -> unrecoverable */
{ assert_release (false); dwb_log_error (...); return ER_FAILED; }
num_recoverable_pages++; /* DWB sane, home torn -> repair */
}
*p_num_recoverable_pages = num_recoverable_pages;

분기 비고: (VPID, LSA) 정렬이 슬롯을 볼륨별로 묶으므로, 디스크립터 / 페이지 수 조회는 볼륨당 한 번만 실행된다; pageid >= vol_pages 건너뜀은 DWB에 있었지만 데이터 볼륨에는 할당된 적 없는 페이지(볼륨이 확장되기 전에 크래시)를 처리한다. 세 개의 상위 에러 분기 중 어느 것이든 호출자의 end: 정리는 여전히 실행된다.

불변 조건 9-C (체크섬 torn 확인된 페이지만 덮어쓴다). home page는 해당 home 페이지가 체크섬 검사에 실패(fileio_page_check_corruption가 손상 보고)하고 동시에 DWB 복사본이 체크섬을 통과할 때 DWB로부터 덮어 써진다. 정상인 home은 쓰기 세트에서 제거된다 — 복구는 torn 쓰기를 복원할 뿐, 정상 페이지를 오래된 이미지로 되돌리지 않는다. home과 DWB 복사본 양쪽 모두 체크섬에 실패하면, 복구는 알려진 불량 페이지를 쓰는 대신 ER_FAILED로 중단된다.

9.8 복구 쓰기, fsync, 그리고 삭제 후 재생성

섹션 제목: “9.8 복구 쓰기, fsync, 그리고 삭제 후 재생성”

dwb_load_and_recover_pages로 돌아와서, dwb_check_data_page_is_sane이 복구 가능한 페이지를 하나라도 발견했을 때만 실제 복구가 실행된다:

// dwb_load_and_recover_pages (repair + fsync) -- src/storage/double_write_buffer.cpp
if (0 < num_recoverable_pages)
{
error_code = dwb_write_block (thread_p, rcv_block, p_dwb_ordered_slots,
ordered_slots_length, false, false);
/* file_sync_helper_can_flush=false, remove_from_hash=false */
if (error_code != NO_ERROR) goto end;
for (i = 0; i < rcv_block->count_flush_volumes_info; i++)
{
if (fileio_synchronize (thread_p, rcv_block->flush_volumes_info[i].vdes, NULL, true)
== NULL_VOLDES)
{ error_code = ER_FAILED; goto end; }
}
rcv_block->count_flush_volumes_info = 0;
}
assert (rcv_block->count_flush_volumes_info == 0);

dwb_write_block (Chapter 6)은 null이 아닌 각 정렬된 슬롯을 해당 home (volid, pageid)에 쓴다. 복구 호출은 뒤쪽 플래그 file_sync_helper_can_flush / remove_from_hashfalse, false를 전달한다: 복구는 아래에서 직접 fsync하고, 퇴출할 슬롯 해시가 없다(vdes == NULL_VOLDES), 반면 런타임 플러시는 true, true를 전달한다. 복구된 각 데이터 볼륨 (flush_volumes_info 내)은 DWB 볼륨이 삭제되기 전에 복구가 지속되도록 fileio_synchronize된다; assert는 목록이 소진됐음을 확인한다.

그런 다음 Branch A의 나머지(§9.2 스켈레톤)가 실행된다: fileio_dismount + fileio_unformat이 DWB 볼륨을 닫고 삭제하며, dwb_create가 복구된 크기가 아닌 설정된 파라미터 크기로 새 빈 DWB를 생성한다 — 여기서 운영자가 변경한 double_write_* 크기가 적용되며, §9.2가 복구된 카운트와의 차이를 허용하는 이유다. 이로써 불변 조건 9-A가 실현된다: torn home이 덮어써지고 fsync됐으며, 구 DWB가 사라지고, 새 빈 DWB가 데몬을 위해 준비된 상태. 오직 이후에야 log_recovery가 신뢰할 수 있는 home 위에서 redo를 수행한다.

9.9 교차 검토 노트 (원시 분석 vs 현재 소스)

섹션 제목: “9.9 교차 검토 노트 (원시 분석 vs 현재 소스)”
  • §9.1 발췌는 축약본이지, 원문 그대로가 아니다. 실제 재시작 경로에는 vacuum_initialize와 복구 호출 사이에 oid_set_root가 있으며, SERVER_MODE 블록은 dwb_daemons_init 이후에 parallel_query::worker_manager_global::get_manager ().init ()도 호출한다. 발췌는 핵심 문장만 유지하며, 생략된 것들은 DWB 순서에 영향을 주지 않는다.
  • dedup dwb_log의 잠재적 포맷 문자열 불일치. 소스의 “Found duplicates” 로그 라인은 세 개%d 지정자("...positions = (%d,%d) %d\n")를 가진 포맷 문자열을 사용하지만 인수는 두 개(s1->position_in_block, s2->position_in_block)만 전달한다. 세 번째 지정자는 미정의 vararg를 읽는다. 실제로는 무해하지만(dwb_check_logging() 하에서만 발동), 이 챕터의 로직 요약이 아닌 소스의 실제 결함이다. §9.5의 발췌는 간결함을 위해 dwb_log 호출을 생략했으므로 버그를 재현하지 않는다.
  • dwb_write_block은 소스 파일에서 재시작-복구 함수들보다 훨씬 위에 정의되어 있다, 인접하게 위치하지 않는다. position-hint 테이블에 검증된 정의 라인이 기록되어 있다.
  1. 부트 내 위치가 전부다. 복구는 vacuum_initialize 이후, dwb_daemons_init / log_initialize 이전에 실행되므로, DWB를 단독으로 점유하고 log_recovery에 일관된 home을 인계한다(불변 조건 9-A). 이 보장을 소비하는 redo 단계는 cubrid-recovery-manager-detail.md를 참고한다.
  2. 두 개의 외부 분기가 모든 경우를 다룬다. DWB 볼륨 없음 -> dwb_create만 실행; 존재하지만 0이거나 2의 거듭제곱이 아님 -> 복구 건너뜀, 재생성; 존재 하고 2의 거듭제곱 -> 실제 복구 경로.
  3. 볼륨은 자기 기술적(self-describing)이다. num_dwb_pages 크기의 DWB_BLOCK 하나, fileio_read_pages 한 번, io_page->prv에서 슬롯 VPID/LSA 재구성 — 외부 온-디스크 인덱스 없음.
  4. 정렬 후 dedup. dwb_block_create_ordered_slotscount_wb_pages개의 실제 슬롯만 (VPID, LSA) 기준으로 정렬하며(불변 조건 9-B), 센티넬은 루프 종료자로서 정렬 범위 밖에 유지된다. dedup 루프는 오래된 복사본을 null로 만들되, 동일 LSA 시 position_in_block으로 판단한다. dwb_debug_check_dwb 는 디버그 빌드에서 결과를 재단언한다.
  5. torn은 체크섬 판정이며, torn 페이지만 덮어쓴다(불변 조건 9-C). dwb_check_data_page_is_sane은 home page에 fileio_page_check_corruption을 실행한다: 정상 home -> 슬롯 null(절대 덮어쓰지 않음); home torn + DWB 복사본 체크섬 유효 -> 복구 대상으로 집계; home torn + DWB 복사본도 손상 -> 치명적 ER_FAILED; 데이터 볼륨 범위 밖 페이지 -> 건너뜀. 읽기 또는 체크 실패는 error_codeend: 정리로 전파한다.
  6. DWB 삭제 전에 복구 내용이 지속된다. dwb_write_block(두 후행 플래그 모두 false)이 체크섬 유효한 DWB 이미지로 torn home을 덮어쓰고, 이후 fileio_synchronizefileio_unformat으로 DWB를 삭제하기 전에 모든 수정된 볼륨을 fsync한다. 에러 발생 시 구 DWB 볼륨은 재시도를 위해 디스크에 남겨진다.
  7. 재생성은 복구된 크기가 아닌 설정된 크기로 이루어진다. dwb_create를 호출해 재생성되며 — 운영자가 재시작으로 DWB 크기를 조정할 수 있는 이유다.

Chapter 10: 구조 변경 대기 큐와 해체

섹션 제목: “Chapter 10: 구조 변경 대기 큐와 해체”

4장부터 9장까지는 페이지의 주 경로(main path) 를 추적했다. 이 장은 그 경로에서 갈라지는 분기들을 다룬다 — 런타임에 DWB를 크기 변경(resize), 재생성(recreate), 또는 파괴(destroy)하는 경로, 그리고 모든 대기 경로가 공유하는 대기 큐(wait queue) 기본 연산들이다. 새로운 구조체는 없다(1장에 DWB_WAIT_QUEUE / DWB_WAIT_QUEUE_ENTRY, 2장에 플래그 비트가 내장된 position 워드가 있다). 이 장이 해부하는 것은 연산이다: CUBRID가 버퍼를 정지(quiesce)시키고, 진행 중인 모든 블록을 소진(drain)하고, 모든 대기자를 깨우고, 볼륨을 재구성하거나 해제하는 방법 — 페이지 손실이나 데드락 없이.

10.1 dwb_starts_structure_modification — 게이트, 소진, 플러시

섹션 제목: “10.1 dwb_starts_structure_modification — 게이트, 소진, 플러시”

create, recreate, destroy는 모두 dwb_starts_structure_modificationdwb_ends_structure_modification 사이에 작업을 배치하여, 다수의 프로듀서와 두 개의 데몬으로 이루어진 시스템을 거친 락(coarse lock) 없이 단일 스레드 임계 구역(critical section)으로 만든다. 게이트는 position_with_flags(2장)의 MODIFY_STRUCTURE 플래그 비트다: 플러시를 시도하는 프로듀서는 이 비트를 확인하고 dwb_wait_for_strucure_modification으로 우회한다. 분기 전체 흐름:

  1. 게이트 — CAS 재시도 루프, 유일하게 미묘한 분기:
    // dwb_starts_structure_modification -- src/storage/double_write_buffer.cpp
    do {
    local_current_position_with_flags = ATOMIC_INC_64 (&dwb_Global.position_with_flags, 0ULL);
    if (DWB_IS_MODIFYING_STRUCTURE (local_current_position_with_flags)) { return ER_FAILED; } /* <- 2nd modifier bails */
    new_position_with_flags = DWB_STARTS_MODIFYING_STRUCTURE (local_current_position_with_flags);
    } while (!ATOMIC_CAS_64 (&dwb_Global.position_with_flags, local_current_position_with_flags, new_position_with_flags));
  2. 소진(Drain) (SERVER_MODE 전용, SA_MODE에서는 no-op) — blocks_flush_counter > 0 이거나 dwb_flush_block_daemon_is_running () 이거나 dwb_file_sync_helper_daemon_is_running () 인 동안 thread_sleep (20) 반복.
  3. 탈취(Steal) — 데몬들이 유휴 상태가 되면 수정자(modifier)가 유일한 DWB 접근자가 되므로, 헬퍼(helper)의 파킹된 블록(있으면)을 가져온다:
    // dwb_starts_structure_modification -- src/storage/double_write_buffer.cpp
    file_sync_helper_block = dwb_Global.file_sync_helper_block;
    if (file_sync_helper_block != NULL)
    { (void) ATOMIC_TAS_ADDR (&dwb_Global.file_sync_helper_block, (DWB_BLOCK *) NULL); } /* <- steal idle helper's block */
  4. 스캔(Scan)position_with_flags를 다시 읽은 후, 가장 오래된(min_version) write-started 블록을 찾아 start_block_no를 결정.
  5. 스윕(Sweep)start_block_no 모듈로 DWB_NUM_TOTAL_BLOCKS부터 순회: write-started 상태이면 dwb_flush_block (..., /*file_sync_helper=*/false, ...)로 인라인 fsync; 오류 시 return error_code(플래그는 유지). blocks_count == 0이 될 때까지 반복.
  6. 사후 확인(Post)assert (DWB_GET_BLOCK_STATUS (...) == 0): write-started 블록이 남아 있지 않음; 오름차순 처리는 복구(9장)와 일치.

불변식 — 동시에 최대 하나의 구조 수정자 (I2). DWB_IS_MODIFYING_STRUCTURE 검사는 CAS 루프 내부에 있다: 두 스레드가 경쟁하면 하나가 이기고, 진 쪽은 다시 읽어 비트를 확인하고 ER_FAILED를 반환한다. 이를 위반하면 두 스레드가 모두 dwb_destroy_internal에 진입해 dwb_Global.blocks를 이중 해제(double-free)한다. 비대칭 설계는 의도적이다 — 두 번째 수정자는 빠르게 실패하고, 두 번째 프로듀서는 대기한다.

불변식 — 소진 중 오류가 발생하면 플래그가 유지됨 (I9). dwb_flush_block이 실패하면 플래그를 지우지 않고 반환한다(dwb_ends_structure_modification만이 플래그를 지우는데, 실패한 호출자는 거기에 도달하지 못한다) — 의도적인 fail-stop 설계. 이를 위반하면 DWB가 오래된 블록을 가진 채로 다시 열린다; 대신 DWB는 동결(frozen) 상태를 유지하고 다음 재시작에서 볼륨을 재실행(replay)한다(9장).

10.2 dwb_ends_structure_modification — 거울 함수

섹션 제목: “10.2 dwb_ends_structure_modification — 거울 함수”

거울 함수는 분기가 없다 — 네 문장으로 구성된다:

// dwb_ends_structure_modification -- src/storage/double_write_buffer.cpp
new_position_with_flags = DWB_ENDS_MODIFYING_STRUCTURE (current_position_with_flags); /* clear flag bit */
assert (dwb_Global.position_with_flags == current_position_with_flags); /* re-affirm I2 */
ATOMIC_TAS_64 (&dwb_Global.position_with_flags, new_position_with_flags); /* plain TAS, no contender */
dwb_signal_structure_modificated (thread_p); /* wake global-queue waiters */

DWB_ENDS_MODIFYING_STRUCTURE는 플래그 비트를 지운다. assert는 단일 수정자 불변식을 재확인한다(플래그가 설정된 상태에서는 다른 스레드가 해당 워드를 쓸 수 없으므로 라이브 값은 여전히 수정자의 스냅샷과 같다). ATOMIC_TAS_64는 이를 공개(publish)한다 — 경쟁자가 없으므로 plain TAS로 충분하다. 마지막으로 dwb_signal_structure_modificated는 전역 큐 대기자들을 깨운다(10.5절).

10.3 dwb_recreate, dwb_destroy — 브래킷 조합

섹션 제목: “10.3 dwb_recreate, dwb_destroy — 브래킷 조합”

두 진입점 모두 dwb_destroy_internal / dwb_create_internal 주위에 브래킷을 배치한다. dwb_recreate는 런타임에 PRM_ID_DWB_SIZE 또는 PRM_ID_DWB_BLOCKS가 변경될 때 시스템 파라미터 기계(system-parameter machinery)에 의해 호출된다. 세 가지 분기: dwb_starts_structure_modification 실패 → return error_code; 그렇지 않고 DWB_IS_CREATED이면 dwb_destroy_internal로 이전 크기를 해제; 그 다음 dwb_create_internal (thread_p, dwb_Volume_name, ...), 오류 시 goto endend:에서 dwb_ends_structure_modification을 실행. “하나의 플래그 아래 destroy 후 create”는 크기 변경을 원자적(atomic)으로 만든다(이전 DWB 또는 새 DWB, 중간 상태는 없다). 기존 dwb_Volume_name을 재사용하므로 인메모리 기하 구조(geometry)만 변경된다.

dwb_destroy세 가지 분기를 가지며, 아래에 인라인으로 표기된다. 분기 3(dwb_daemons_destroy)은 dwb_recreate가 생략하는 부분으로, 재생성된 버퍼는 여전히 데몬들이 필요하기 때문이다:

// dwb_destroy -- src/storage/double_write_buffer.cpp
error_code = dwb_starts_structure_modification (thread_p, &current_position_with_flags);
if (error_code != NO_ERROR) { return error_code; } /* <- branch 1: another modifier holds the flag, bail */
if (!DWB_IS_CREATED (current_position_with_flags)) { goto end; } /* <- branch 2: nothing to destroy, release flag */
dwb_destroy_internal (thread_p, &current_position_with_flags);
end:
dwb_ends_structure_modification (thread_p, current_position_with_flags);
#if defined(SERVER_MODE)
dwb_daemons_destroy (); /* <- branch 3: full shutdown only, not on recreate */
#endif

dwb_destroy_internal(3장)이 핵심 작업을 수행한다: dwb_destroy_wait_queue (&dwb_Global.wait_queue, ...)전역 큐를 소진하고 mutex를 해제, 모든 블록에 dwb_finalize_block을 적용, 슬롯(slot) 해시맵을 해제, 볼륨을 언마운트/언포맷한 뒤 position 워드를 reset+ended-creation 상태로 CAS 복귀. SA 모드와 서버 모드의 차이는 #if defined(SERVER_MODE) 가드에서 비롯된다 — SA_MODE에는 데몬이 없으므로 dwb_daemons_destroy와 10.1절의 소진 루프가 no-op이다.

대기 큐는 자체 제작된 단일 연결 리스트(singly-linked list)에 내장 free list를 결합한 구조로, 블록별 대기자(DWB_BLOCK::wait_queue)와 전역 구조 변경 대기자(dwb_Global.wait_queue) 양쪽에 재사용된다. 모든 기본 연산은 재진입 불가(non-reentrant)이며, 호출자가 mutex로 직렬화한다. dwb_init_wait_queue는 두 리스트를 모두 초기화(zero)한다. dwb_make_wait_queue_entry는 free list 빠른 경로(fast path)로 할당한다(비어 있지 않으면 free_list에서 pop, 그렇지 않으면 mallocmalloc 분기만 실패할 수 있으며 NULL을 반환). dwb_block_add_wait_queue_entry는 이를 감싸며, 빈 큐 분기(head == NULL이면 headtail 모두 설정)와 함께 꼬리(tail)에 추가한다.

dwb_block_disconnect_wait_queue_entry는 분기가 가장 많은 기본 연산이다 — 데이터 포인터로 노드를 찾아 연결을 끊는다(data == NULL이면 head를 제거). 아래에 인라인으로 표기된 네 가지 종료 분기:

// dwb_block_disconnect_wait_queue_entry -- src/storage/double_write_buffer.cpp
if (wait_queue->head == NULL) { return NULL; } /* <- branch 1: empty */
prev_wait_queue_entry = NULL; wait_queue_entry = wait_queue->head;
if (data != NULL) /* <- targeted search; else head is taken */
{ /* ... walk next, tracking prev, until ->data == data ... */ }
if (wait_queue_entry == NULL) { return NULL; } /* <- branch 2: data not found */
if (prev_wait_queue_entry != NULL)
{ prev_wait_queue_entry->next = wait_queue_entry->next; } /* <- branch 3: middle/tail */
else
{ wait_queue->head = wait_queue_entry->next; } /* <- branch 4: head */
if (wait_queue_entry == wait_queue->tail)
{ wait_queue->tail = prev_wait_queue_entry; } /* <- fix tail */
wait_queue_entry->next = NULL; wait_queue->count--;

불변식 — head/tail/count가 일치함 (I6). 모든 기본 연산 이후 counthead에서 도달 가능한 노드 수와 같아야 하고, tail은 마지막 노드(빈 경우 NULL)이며, tail->next == NULL이다. disconnect는 꼬리 제거 시 tail을 수정하고, add는 추가 시 갱신한다. 이를 위반하면 dwb_signal_waiting_threads의 소진 루프(head == NULL에서 종료)가 무한 루프에 빠지거나 해제된 엔트리를 깨울 수 있다.

dwb_block_free_wait_queue_entry는 실제로 메모리를 해제하지 않는다 — NULL 엔트리에서는 즉시 반환하고(이것이 disconnectNULL을 반환할 때 dwb_remove_wait_queue_entry를 안전하게 만드는 이유), 선택적 콜백 이후 엔트리를 free_list에 push한다. data는 디버깅을 위해 그대로 유지된다. dwb_remove_wait_queue_entry는 선택적 mutex 아래에서 disconnect와 free를 조합한다 — 타임아웃 및 인터럽트 경로가 호출하는 빌딩 블록이다.

dwb_signal_waiting_threads전체 큐를 소진한다 — while (wait_queue->head != NULL) dwb_remove_wait_queue_entry (wait_queue, NULL, NULL, dwb_signal_waiting_thread); — 내부 mutex에 NULL을 전달(이미 보유 중)하고 NULL data로 호출해 매번 head를 제거한다. dwb_signal_waiting_thread는 엔트리별 콜백이다:

// dwb_signal_waiting_thread -- src/storage/double_write_buffer.cpp
wait_thread_p = (THREAD_ENTRY *) wait_queue_entry->data;
if (wait_thread_p) { /* <- null-data guard: skip cleared entry */
thread_lock_entry (wait_thread_p);
if (wait_thread_p->resume_status == THREAD_DWB_QUEUE_SUSPENDED) /* <- still waiting -> no double-wake */
{ thread_wakeup_already_had_mutex (wait_thread_p, THREAD_DWB_QUEUE_RESUMED); }
thread_unlock_entry (wait_thread_p);
}

두 가드(인라인)는 지워진 엔트리를 건너뛰고, 이미 타임아웃으로 자체 깨어난 스레드를 이중 깨움(double-wake)하는 것을 방지한다. dwb_set_status_resumed다른 콜백(타임아웃 경로)이다: 깨우지 않고 resume_statusTHREAD_DWB_QUEUE_RESUMED로 설정한다 — 스레드가 스스로 깨어난 것이다. 두 콜백 모두 SERVER_MODE 외부에서는 no-op이다. dwb_signal_block_completiondwb_signal_structure_modificated는 발생 시점에 이름을 부여하는 얇은 래퍼들이다(블록별 큐는 플러시 완료 시, 6장; 전역 큐는 수정 종료 시, 10.2절). 둘 다 dwb_signal_waiting_threads에 위임한다. dwb_destroy_wait_queue는 해체(teardown) 연산이다: 모든 대기자에게 신호를 보낸 후 free_list를 순회하며 재활용된 모든 엔트리를 free()한다 — 힙(heap)으로 실제 메모리를 해제하는 유일한 기본 연산.

10.6 대기자 측 — dwb_wait_for_block_completion

섹션 제목: “10.6 대기자 측 — dwb_wait_for_block_completion”

write-started 블록의 플러시를 기다려야 하는 프로듀서가 여기에 파킹된다. mutex 아래에서 재확인하고(블록이 읽기와 락 사이에 이미 완료되었을 수 있음 — 빠른 return NO_ERROR), 자신을 큐에 추가하고(dwb_block_add_wait_queue_entry; NULL → 할당 오류), 20 ms 타임아웃으로 정지한 후, 세 가지 재개 결과를 처리한다:

// dwb_wait_for_block_completion -- src/storage/double_write_buffer.cpp
// ... re-check DWB_IS_BLOCK_WRITE_STARTED -> return NO_ERROR if done; add entry, NULL -> alloc error ...
r = thread_suspend_timeout_wakeup_and_unlock_entry (thread_p, &to, THREAD_DWB_QUEUE_SUSPENDED);
if (r == ER_CSS_PTHREAD_COND_TIMEDOUT) /* <- timeout: self-dequeue + dwb_set_status_resumed */
{ dwb_remove_wait_queue_entry (&dwb_block->wait_queue, &dwb_block->mutex, thread_p, dwb_set_status_resumed); return r; }
else if (thread_p->resume_status != THREAD_DWB_QUEUE_RESUMED) /* <- shutdown interrupt: self-dequeue, no callback */
{ assert (thread_p->resume_status == THREAD_RESUME_DUE_TO_SHUTDOWN);
dwb_remove_wait_queue_entry (&dwb_block->wait_queue, &dwb_block->mutex, thread_p, NULL);
er_set (..., ER_INTERRUPTED, 0); return ER_INTERRUPTED; }
else { return NO_ERROR; } /* <- normal wake; signaller already dequeued */

다섯 가지 종료 분기: 이미 완료, 할당 오류, 타임아웃, 셧다운 인터럽트, 정상 깨움. 전역 큐 쌍둥이 dwb_wait_for_strucure_modification(10.1절)은 두 가지 차이점과 함께 이를 거울처럼 반영한다: 10 ms 타임아웃, 그리고 타임아웃 분기에서 dwb_set_status_resumed 대신 NULL을 콜백으로 전달한다 — 전역 대기자는 수정이 끝나면 단순히 플래그를 재확인하고 진행하므로 자신의 resume_status를 다시 스탬프할 필요가 없다.

불변식 — 대기자는 한 번 큐에 들어가고 정확히 한 번 빠져나옴 (I5). 블록 mutex가 큐 추가를 직렬화하고, 재개 분기가 누가 큐에서 제거하는지를 결정한다(타임아웃/인터럽트는 자체 제거, 정상 깨움은 신호자가 제거). 이를 위반하면 이중 제거(double-dequeue)로 리스트가 손상된다; dwb_block_disconnect_wait_queue_entry가 이미 제거된 엔트리에 NULL을 반환하고 dwb_block_free_wait_queue_entry가 아무것도 해제하지 않아 방지된다.

10.7 관찰 가능성과 임시 볼륨 단락(short-circuit)

섹션 제목: “10.7 관찰 가능성과 임시 볼륨 단락(short-circuit)”

dwb_is_created는 모든 쓰기가 참조하는 저렴한 공개 술어(predicate)다 — 원자적 읽기 하나와 DWB_IS_CREATED 매크로. dwb_get_volume_name은 이 술어에 따라 이름을 제공하며, DWB가 없으면 NULL을 반환해 호출자가 오래된 경로를 참조하지 않도록 한다.

임시 볼륨 단락은 호출자 측에 있다: file_io.c에서 dwb_is_created ()fileio_traverse_permanent_volume으로 대상이 영구적(permanent) 볼륨임을 확인한 후에만 쓰기를 dwb_add_page로 라우팅한다. 임시 볼륨은 직접 쓰기(direct write)로 떨어진다 — 재시작 시 폐기되므로 DWB 쓰기 증폭(write-amplification)(상위 수준 설명 참조)은 낭비가 된다. DWB 자체 볼륨은 예약 ID LOG_DBDWB_VOLID(log_volids.hpp= LOG_DBFIRST_VOLID - 22)로 격리되어 있으며, 프로듀서가 스캔하는 데이터 볼륨 ID 공간 밖의 음수 시스템 볼륨 범위에 속한다 — 동일한 술어만으로 DWB 볼륨이 자기 자신에 버퍼링되는 것을 막는다.

10.8 횡단 불변식 — 장들을 하나로 묶다

섹션 제목: “10.8 횡단 불변식 — 장들을 하나로 묶다”
#불변식보장 방법
I1position_with_flags가 단일 진실 공급원ATOMIC_CAS_64 / ATOMIC_TAS_64 뮤테이터2, 10
I2동시에 최대 하나의 구조 수정자CAS 루프 내 DWB_IS_MODIFYING_STRUCTURE; 패배자 ER_FAILED10
I3각 블록은 버전당 한 번, 오름차순으로 플러시블록 상태 비트 + blocks_flush_counter + 버전 스캔5, 6, 10
I4(volid,pageid) 슬롯은 최고 LSA 버전을 보유LSA 순서 dwb_slots_hash_insert5
I5대기자는 한 번 큐 진입, 한 번 큐 이탈블록/전역 mutex + resume_status 분기8, 10
I6대기 큐 head/tail/count가 일관성 유지disconnect의 tail 수정, add의 tail 갱신10
I7수정은 데몬들이 유휴 상태일 때만 진행카운터 + is_running()에 대한 SERVER_MODE 소진 루프7, 10
I8영구 볼륨만 DWB로 라우팅fileio_traverse_permanent_volume; LOG_DBDWB_VOLID8, 10
I9소진 실패 시 DWB가 동결됨 (플래그 유지)dwb_ends_structure_modification 이전 조기 return error_code9, 10
  1. 하나의 플래그 비트가 구조 변경을 직렬화한다(I2): 두 번째 수정자는 빠르게 실패하고(ER_FAILED), 프로듀서는 대기한다 — 비대칭 설계는 의도적이다.
  2. 수정자는 메모리에 접근하기 전에 소진을 수행하고(I7), write-started 블록을 오름차순으로 플러시하며 인라인으로 fsync한다. 소진 실패는 fail-stop으로, 플래그를 유지해 복구가 볼륨을 재실행하게 한다(I9).
  3. dwb_recreate는 하나의 플래그 아래 destroy 후 create(원자적 크기 변경); dwb_destroy는 starts-failed 조기 반환에 더해 생성되지 않은 경우의 단락과 데몬 해체를 추가한다.
  4. 대기 큐는 free_list로 재활용되고(NULL free는 no-op), dwb_destroy_wait_queue만이 힙으로 실제 해제한다. head/tail/count는 일관성을 유지한다(I6).
  5. 재개에는 세 가지 분기가 있다(I5) — 타임아웃, 셧다운 인터럽트, 정상 신호 — 대기자는 한 번 큐에 들어가고 한 번 빠져나온다. dwb_signal_waiting_thread는 엔트리 data의 null 가드를 수행한다.
  6. DWB는 자기 자신과 임시 볼륨을 제외한다(I8): 영구 볼륨만 버퍼링되고, DWB 자체 볼륨은 예약 LOG_DBDWB_VOLID에 위치한다. dwb_is_created / dwb_get_volume_name이 관찰 가능성의 게이트 역할을 한다.

아래 줄 번호는 2026-06-23 시점에 관찰한 값이다. 심볼 이름이 정식 앵커이며 줄 번호는 시간이 지나면 어긋나는 힌트다.

SymbolFileLine
dwb_flush_force (disk_manager call site)src/storage/disk_manager.c733
dwb_synchronize (disk_manager call site)src/storage/disk_manager.c780
DWB_SLOTS_HASH_SIZEsrc/storage/double_write_buffer.cpp47
DWB_SLOTS_FREE_LIST_SIZEsrc/storage/double_write_buffer.cpp48
DWB_MIN_SIZEsrc/storage/double_write_buffer.cpp51
DWB_MAX_SIZEsrc/storage/double_write_buffer.cpp52
DWB_MIN_BLOCKSsrc/storage/double_write_buffer.cpp53
DWB_MAX_BLOCKSsrc/storage/double_write_buffer.cpp54
DWB_NUM_TOTAL_BLOCKSsrc/storage/double_write_buffer.cpp57
DWB_NUM_TOTAL_BLOCKSsrc/storage/double_write_buffer.cpp57
DWB_NUM_TOTAL_PAGESsrc/storage/double_write_buffer.cpp60
DWB_BLOCK_NUM_PAGESsrc/storage/double_write_buffer.cpp63
DWB_BLOCK_NUM_PAGESsrc/storage/double_write_buffer.cpp63
DWB_LOG2_BLOCK_NUM_PAGESsrc/storage/double_write_buffer.cpp66
DWB_POSITION_MASKsrc/storage/double_write_buffer.cpp70
DWB_BLOCKS_STATUS_MASKsrc/storage/double_write_buffer.cpp73
DWB_MODIFY_STRUCTUREsrc/storage/double_write_buffer.cpp76
DWB_CREATEsrc/storage/double_write_buffer.cpp79
DWB_CREATE_OR_MODIFY_MASKsrc/storage/double_write_buffer.cpp82
DWB_FLAG_MASKsrc/storage/double_write_buffer.cpp85
DWB_GET_POSITIONsrc/storage/double_write_buffer.cpp89
DWB_RESET_POSITIONsrc/storage/double_write_buffer.cpp93
DWB_GET_BLOCK_STATUSsrc/storage/double_write_buffer.cpp97
DWB_GET_BLOCK_NO_FROM_POSITIONsrc/storage/double_write_buffer.cpp101
DWB_GET_BLOCK_NO_FROM_POSITIONsrc/storage/double_write_buffer.cpp101
DWB_IS_BLOCK_WRITE_STARTEDsrc/storage/double_write_buffer.cpp105
DWB_IS_BLOCK_WRITE_STARTEDsrc/storage/double_write_buffer.cpp105
DWB_IS_ANY_BLOCK_WRITE_STARTEDsrc/storage/double_write_buffer.cpp109
DWB_IS_ANY_BLOCK_WRITE_STARTEDsrc/storage/double_write_buffer.cpp109
DWB_STARTS_BLOCK_WRITINGsrc/storage/double_write_buffer.cpp113
DWB_STARTS_BLOCK_WRITINGsrc/storage/double_write_buffer.cpp113
DWB_ENDS_BLOCK_WRITINGsrc/storage/double_write_buffer.cpp117
DWB_ENDS_BLOCK_WRITINGsrc/storage/double_write_buffer.cpp117
DWB_STARTS_MODIFYING_STRUCTUREsrc/storage/double_write_buffer.cpp122
DWB_ENDS_MODIFYING_STRUCTUREsrc/storage/double_write_buffer.cpp126
DWB_IS_MODIFYING_STRUCTUREsrc/storage/double_write_buffer.cpp130
DWB_IS_MODIFYING_STRUCTUREsrc/storage/double_write_buffer.cpp130
DWB_STARTS_CREATIONsrc/storage/double_write_buffer.cpp134
DWB_ENDS_CREATIONsrc/storage/double_write_buffer.cpp138
DWB_IS_CREATEDsrc/storage/double_write_buffer.cpp142
DWB_IS_CREATEDsrc/storage/double_write_buffer.cpp142
DWB_NOT_CREATED_OR_MODIFYINGsrc/storage/double_write_buffer.cpp146
DWB_NOT_CREATED_OR_MODIFYINGsrc/storage/double_write_buffer.cpp146
DWB_GET_NEXT_POSITION_WITH_FLAGSsrc/storage/double_write_buffer.cpp150
DWB_GET_NEXT_POSITION_WITH_FLAGSsrc/storage/double_write_buffer.cpp150
DWB_GET_POSITION_IN_BLOCKsrc/storage/double_write_buffer.cpp155
DWB_GET_POSITION_IN_BLOCKsrc/storage/double_write_buffer.cpp155
double_write_wait_queue_entrysrc/storage/double_write_buffer.cpp176
double_write_wait_queuesrc/storage/double_write_buffer.cpp184
DWB_WAIT_QUEUE_INITIALIZERsrc/storage/double_write_buffer.cpp193
FLUSH_VOLUME_STATUSsrc/storage/double_write_buffer.cpp196
flush_volume_infosrc/storage/double_write_buffer.cpp204
double_write_blocksrc/storage/double_write_buffer.cpp215
dwb_slots_hash_entrysrc/storage/double_write_buffer.cpp235
dwb_hashmap_typesrc/storage/double_write_buffer.cpp257
double_write_buffersrc/storage/double_write_buffer.cpp261
next_block_to_flushsrc/storage/double_write_buffer.cpp270
position_with_flagssrc/storage/double_write_buffer.cpp275
slots_hashmapsrc/storage/double_write_buffer.cpp278
dwb_Globalsrc/storage/double_write_buffer.cpp306
dwb_flush_block_daemonsrc/storage/double_write_buffer.cpp401
dwb_file_sync_helper_daemonsrc/storage/double_write_buffer.cpp402
slots_entry_Descriptorsrc/storage/double_write_buffer.cpp421
dwb_init_wait_queuesrc/storage/double_write_buffer.cpp451
dwb_make_wait_queue_entrysrc/storage/double_write_buffer.cpp469
dwb_block_add_wait_queue_entrysrc/storage/double_write_buffer.cpp506
dwb_block_disconnect_wait_queue_entrysrc/storage/double_write_buffer.cpp541
dwb_block_free_wait_queue_entrysrc/storage/double_write_buffer.cpp603
dwb_remove_wait_queue_entrysrc/storage/double_write_buffer.cpp634
dwb_signal_waiting_threadssrc/storage/double_write_buffer.cpp663
dwb_destroy_wait_queuesrc/storage/double_write_buffer.cpp691
dwb_power2_ceilsrc/storage/double_write_buffer.cpp732
dwb_load_buffer_sizesrc/storage/double_write_buffer.cpp769
dwb_load_block_countsrc/storage/double_write_buffer.cpp795
dwb_starts_structure_modificationsrc/storage/double_write_buffer.cpp822
dwb_starts_structure_modificationsrc/storage/double_write_buffer.cpp822
structure-modification spin loopsrc/storage/double_write_buffer.cpp847
dwb_ends_structure_modificationsrc/storage/double_write_buffer.cpp924
dwb_initialize_slotsrc/storage/double_write_buffer.cpp949
dwb_initialize_blocksrc/storage/double_write_buffer.cpp978
dwb_create_blockssrc/storage/double_write_buffer.cpp1009
dwb_finalize_blocksrc/storage/double_write_buffer.cpp1131
dwb_create_internalsrc/storage/double_write_buffer.cpp1163
dwb_create_internalsrc/storage/double_write_buffer.cpp1163
log2_num_block_pagessrc/storage/double_write_buffer.cpp1213
dwb_slots_hash_entry_allocsrc/storage/double_write_buffer.cpp1261
dwb_slots_hash_entry_freesrc/storage/double_write_buffer.cpp1283
dwb_slots_hash_entry_initsrc/storage/double_write_buffer.cpp1304
dwb_slots_hash_key_copysrc/storage/double_write_buffer.cpp1326
dwb_slots_hash_compare_keysrc/storage/double_write_buffer.cpp1345
dwb_slots_hash_keysrc/storage/double_write_buffer.cpp1363
dwb_slots_hash_insertsrc/storage/double_write_buffer.cpp1380
dwb_destroy_internalsrc/storage/double_write_buffer.cpp1474
dwb_set_status_resumedsrc/storage/double_write_buffer.cpp1521
dwb_wait_for_block_completionsrc/storage/double_write_buffer.cpp1552
dwb_wait_for_block_completionsrc/storage/double_write_buffer.cpp1552
dwb_signal_waiting_threadsrc/storage/double_write_buffer.cpp1643
dwb_signal_block_completionsrc/storage/double_write_buffer.cpp1676
dwb_signal_structure_modificatedsrc/storage/double_write_buffer.cpp1691
dwb_wait_for_strucure_modificationsrc/storage/double_write_buffer.cpp1704
dwb_wait_for_strucure_modificationsrc/storage/double_write_buffer.cpp1704
dwb_compare_slotssrc/storage/double_write_buffer.cpp1781
dwb_block_create_ordered_slotssrc/storage/double_write_buffer.cpp1845
dwb_slots_hash_deletesrc/storage/double_write_buffer.cpp1883
dwb_compare_vol_fdsrc/storage/double_write_buffer.cpp1938
dwb_add_volume_to_block_flush_areasrc/storage/double_write_buffer.cpp1960
dwb_write_blocksrc/storage/double_write_buffer.cpp2007
dwb_write_block mid-stream wakeupsrc/storage/double_write_buffer.cpp2105
dwb_write_block tail wakeupsrc/storage/double_write_buffer.cpp2136
dwb_flush_blocksrc/storage/double_write_buffer.cpp2192
dwb_flush_block pre-write drain loopsrc/storage/double_write_buffer.cpp2275
dwb_flush_block post-write per-volume syncsrc/storage/double_write_buffer.cpp2361
dwb_flush_block next_block_to_flush CASsrc/storage/double_write_buffer.cpp2433
dwb_acquire_next_slotsrc/storage/double_write_buffer.cpp2468
dwb_acquire_next_slotsrc/storage/double_write_buffer.cpp2468
dwb_set_slot_datasrc/storage/double_write_buffer.cpp2612
dwb_init_slotsrc/storage/double_write_buffer.cpp2642
dwb_get_next_block_for_flushsrc/storage/double_write_buffer.cpp2659
dwb_set_data_on_next_slotsrc/storage/double_write_buffer.cpp2686
dwb_add_pagesrc/storage/double_write_buffer.cpp2726
dwb_flush_block flush-daemon wakeup sitesrc/storage/double_write_buffer.cpp2807
dwb_synchronizesrc/storage/double_write_buffer.cpp2841
dwb_is_createdsrc/storage/double_write_buffer.cpp2909
dwb_createsrc/storage/double_write_buffer.cpp2925
dwb_recreatesrc/storage/double_write_buffer.cpp2967
dwb_debug_check_dwbsrc/storage/double_write_buffer.cpp3013
dwb_check_data_page_is_sanesrc/storage/double_write_buffer.cpp3091
dwb_load_and_recover_pagessrc/storage/double_write_buffer.cpp3199
dwb_destroysrc/storage/double_write_buffer.cpp3403
dwb_get_volume_namesrc/storage/double_write_buffer.cpp3440
dwb_flush_next_blocksrc/storage/double_write_buffer.cpp3459
dwb_flush_forcesrc/storage/double_write_buffer.cpp3514
dwb_flush_forcesrc/storage/double_write_buffer.cpp3514
dwb_file_sync_helpersrc/storage/double_write_buffer.cpp3766
dwb_read_pagesrc/storage/double_write_buffer.cpp3969
dwb_flush_block_daemon_tasksrc/storage/double_write_buffer.cpp4013
dwb_flush_block_daemon_task::executesrc/storage/double_write_buffer.cpp4024
dwb_file_sync_helper_executesrc/storage/double_write_buffer.cpp4053
dwb_flush_block_daemon_initsrc/storage/double_write_buffer.cpp4073
dwb_file_sync_helper_daemon_initsrc/storage/double_write_buffer.cpp4087
dwb_daemons_initsrc/storage/double_write_buffer.cpp4099
dwb_daemons_destroysrc/storage/double_write_buffer.cpp4109
dwb_is_flush_block_daemon_availablesrc/storage/double_write_buffer.cpp4121
dwb_is_file_sync_helper_daemon_availablesrc/storage/double_write_buffer.cpp4135
dwb_flush_block_daemon_is_runningsrc/storage/double_write_buffer.cpp4150
dwb_file_sync_helper_daemon_is_runningsrc/storage/double_write_buffer.cpp4166
double_write_slotsrc/storage/double_write_buffer.hpp33
fileio_unformatsrc/storage/file_io.c2689
fileio_copy_volumesrc/storage/file_io.c2776
fileio_reset_volumesrc/storage/file_io.c2876
fileio_mountsrc/storage/file_io.c2934
fileio_dismountsrc/storage/file_io.c3111
fileio_traverse_permanent_volumesrc/storage/file_io.c3218
fileio_dismount_volumesrc/storage/file_io.c3327
fileio_readsrc/storage/file_io.c3935
fileio_write_or_add_to_dwbsrc/storage/file_io.c4009
fileio_writesrc/storage/file_io.c4135
fileio_read_pagessrc/storage/file_io.c4211
fileio_write_pagessrc/storage/file_io.c4297
fileio_fsync_pendingsrc/storage/file_io.c4416
fileio_synchronizesrc/storage/file_io.c4455
fileio_synchronize_allsrc/storage/file_io.c4625
fileio_get_number_of_volume_pagessrc/storage/file_io.c4919
fileio_is_volume_existsrc/storage/file_io.c5095
fileio_make_dwb_namesrc/storage/file_io.c5882
fileio_get_volume_descriptorsrc/storage/file_io.c6489
fileio_initialize_ressrc/storage/file_io.c11670
fileio_page_check_corruptionsrc/storage/file_io.c11924
FILEIO_SUFFIX_DWBsrc/storage/file_io.h92
dwb_read_page (pgbuf fix call site)src/storage/page_buffer.c8239
pgbuf_bcb_safe_flush_internalsrc/storage/page_buffer.c8550
pgbuf_bcb_flush_with_walsrc/storage/page_buffer.c10456
uses_dwbsrc/storage/page_buffer.c10527
dwb_set_data_on_next_slot (call site)src/storage/page_buffer.c10548
logpb_flush_log_for_wal (call site)src/storage/page_buffer.c10572
dwb_add_page (pgbuf call site)src/storage/page_buffer.c10597
REGISTER_DAEMONsrc/thread/thread_manager.hpp498
dwb_load_and_recover_pages call sitesrc/transaction/boot_sr.c2403
dwb_create call sitesrc/transaction/boot_sr.c4908
BO_IS_FLUSH_DAEMON_AVAILABLEsrc/transaction/boot_sr.h89
log_initializesrc/transaction/log_manager.c1059
LOG_DBDWB_VOLIDsrc/transaction/log_volids.hpp57
  • cubrid-double-write-buffer.md — 상위 수준 동반 문서. 플러시 생산자 계층에 대해서는 cubrid-page-buffer-manager.md도 참고.
  • 코드: src/storage/double_write_buffer.{cpp,hpp}, 플러시 호출 지점은 src/storage/page_buffer.c, I/O는 src/storage/file_io.c.
  • 방법론: knowledge/methodology/code-analysis-detail-doc.md.