Skip to content

[CBRD-26609] implement OOS delete API#6909

Draft
vimkim wants to merge 2 commits intoCUBRID:feat/oosfrom
vimkim:cbrd-26609-oos-delete
Draft

[CBRD-26609] implement OOS delete API#6909
vimkim wants to merge 2 commits intoCUBRID:feat/oosfrom
vimkim:cbrd-26609-oos-delete

Conversation

@vimkim
Copy link
Contributor

@vimkim vimkim commented Mar 16, 2026

http://jira.cubrid.org/browse/CBRD-26609

Description

Milestone 1에서 OOS insert/read는 구현되어 있으나, OOS 레코드를 물리적으로 삭제하는 oos_delete API가 없었다.

이로 인해 아래 두 경로에서 OOS 레코드가 영구히 잔존하는 문제가 있었다.

경로 문제
UPDATE 이전 버전 OOS 레코드가 삭제되지 않고 남음
DELETE + vacuum vacuum이 heap record를 정리하더라도 OOS 레코드가 orphan으로 잔존

spage_delete를 통해 OOS 페이지의 슬롯을 물리적으로 삭제하고, 페이지의 total_free를 회수하는 oos_delete API를 구현한다.


Implementation

신규 함수

함수 파일 설명
oos_delete src/storage/oos_file.cpp OOS 레코드 물리 삭제 (public API)
oos_log_delete_physical src/storage/oos_file.cpp OOS 삭제 WAL 로깅 내부 헬퍼 (static)

oos_delete 동작 흐름

Multi-chunk OOS 레코드(across-pages)를 지원하기 위해 next_chunk_oid 체인을 순회하며 모든 청크를 순서대로 삭제한다.

while (current_oid.pageid != NULL_PAGEID):
    pgbuf_fix (WRITE latch)
    spage_get_record (PEEK) → OOS_RECORD_HEADER에서 next_chunk_oid 확인
    oos_log_delete_physical 호출 (WAL 선행 기록)
    spage_delete → total_free 증가
    pgbuf_set_dirty + pgbuf_unfix
    current_oid = next_chunk_oid

WAL 로깅 설계

RVOOS_DELETE 로그 레코드는 이미 recovery.h에 정의되어 있으며, 핸들러도 등록되어 있다.

필드 내용
log_addr.pgptr 삭제 대상 OOS 페이지
log_addr.offset slotid (redo 시 oos_rv_redo_delete가 이 값 사용)
undo data 원본 RECDES (rollback 시 oos_rv_redo_insert로 레코드 재삽입)
redo data 없음 (slotid만으로 redo 가능)

기존 recovery 테이블에 등록된 핸들러를 그대로 활용한다:

  • undo: oos_rv_redo_insert (원본 레코드 재삽입)
  • redo: oos_rv_redo_delete (슬롯 삭제)

단위 테스트 (unit_tests/oos/test_oos_delete.cpp)

테스트 검증 내용
OosDeleteBasic oos_delete 성공, 삭제 후 페이지 free space 증가 확인
OosDeleteThenReadFails 삭제된 OID로 oos_read 시 에러 반환 확인
OosDeleteMultiChunk 2-chunk 레코드 삭제 시 양쪽 페이지 모두 free space 증가 확인
OosUpdatePattern UPDATE 패턴 시뮬레이션: 이전 레코드 삭제 후 새 레코드 정상 읽기, 이전 OID 읽기 실패 확인
OosDeleteRestoresFreeSpace 삭제 후 레코드 데이터 크기만큼 free space 회수 확인
OosDeleteLarge160KBMultiChunk 160KB multi-chunk 레코드 삭제 후 head OID 읽기 실패 확인
image

모두 통과한 모습


Remarks

  • spage_delete는 레코드 데이터는 해제하지만 슬롯 엔트리(SPAGE_SLOT, 4 bytes)는 REC_DELETED_WILL_REUSE 상태로 남긴다. 추후 spage_compact를 통한 in-page compaction에서 이 공간을 재활용할 수 있다.
  • 호출 경로 연결(heap_updateoos_delete, vacuum_heapoos_delete)은 이 PR 범위에 포함되지 않는다. 해당 연결은 상위 스토리에서 진행한다.

@github-actions
Copy link

🧪 TC Test Environment Ready

CircleCI Testing:

  • CircleCI will automatically test using the branches below.

TC Repositories & Branches:

Next Steps:

  1. Wait for CircleCI tests to complete
  2. If CircleCI tests failed, please check the test results and fix the issues.
  3. When ready to merge this PR, please merge the TC PR first, then merge this PR.

@vimkim
Copy link
Contributor Author

vimkim commented Mar 16, 2026

/run sql medium

Copy link
Contributor Author

@vimkim vimkim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전체적인 구현 리뷰입니다. 주요 포인트를 각 라인 코멘트로 남겼습니다.

static void
oos_log_delete_physical (THREAD_ENTRY *thread_p, PAGE_PTR page_p, PGSLOTID slotid, RECDES *recdes_p)
{
LOG_DATA_ADDR log_addr;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WAL 설계] undo/redo 역할 분담

RVOOS_DELETE 로그의 undo/redo 구성:

  • undo data (recdes_p) — rollback 시 oos_rv_redo_insert가 이 레코드를 그대로 재삽입하여 삭제 이전 상태로 복원
  • redo data (NULL) — crash recovery 시 oos_rv_redo_deletercv->offset(= slotid)만으로 spage_delete를 재실행할 수 있으므로 별도 데이터 불필요

recovery.c 테이블에 이미 등록된 핸들러:

RVOOS_DELETE → undo: oos_rv_redo_insert / redo: oos_rv_redo_delete

신규 핸들러 추가 없이 기존 인프라를 그대로 활용한다.

}

int
oos_delete (THREAD_ENTRY *thread_p, const VFID &oos_vfid, const OID &oid)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[oos_delete] multi-chunk 체인 순회

OOS 레코드가 여러 페이지에 걸쳐 저장된 경우(across-pages), next_chunk_oid를 따라 연결된 모든 청크를 순서대로 삭제한다.

체인 종료 조건: next_chunk_oid.pageid == NULL_PAGEID (마지막 청크의 헤더에 저장된 값)

반복 흐름:

head → chunk[0] → chunk[1] → ... → chunk[N] (next=NULL)

각 청크를 독립적으로 fix → log → delete → unfix 처리한다.

return ER_FAILED;
}

scope_exit page_unfixer ([&] ()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[RAII] scope_exit로 페이지 unfix 보장

scope_exit를 사용하여 이후 경로에서 에러가 발생하더라도 반드시 pgbuf_unfix가 호출되도록 한다.

pgbuf_unfix_and_init_after_check는 unfix 후 포인터를 nullptr로 초기화하여 dangling pointer 접근을 방지한다.

});

RECDES recdes_with_header;
SCAN_CODE code = spage_get_record (thread_p, page_ptr, slotid, &recdes_with_header, PEEK);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[순서] PEEK → next_chunk_oid 확보 → log → delete

spage_delete 호출 이전에 반드시 spage_get_record(PEEK)를 먼저 수행해야 한다.

이유: OOS_RECORD_HEADER 안에 있는 next_chunk_oid는 삭제 후에는 읽을 수 없다. PEEK로 헤더를 미리 복사해 두어야 다음 청크로 이동할 수 있다.

또한 PEEK로 가져온 recdes_with_header가 WAL undo data로 그대로 전달되므로, 별도 복사 없이 효율적으로 처리된다.

std::memcpy (&header, recdes_with_header.data, sizeof (OOS_RECORD_HEADER));
OID next_chunk_oid = header.next_chunk_oid;

oos_log_delete_physical (thread_p, page_ptr, slotid, &recdes_with_header);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WAL 원칙] 로그는 반드시 실제 변경 이전에 기록

spage_delete 호출 전에 oos_log_delete_physical을 먼저 호출하는 것은 WAL(Write-Ahead Logging) 원칙을 지키기 위함이다.

crash가 로그 기록과 spage_delete 사이에 발생하면: redo 로그가 없으므로 삭제가 재실행되지 않고 레코드가 보존됨 → 안전
crash가 spage_delete 이후에 발생하면: redo 로그로 spage_delete를 재실행하여 일관성 유지

extern int oos_file_destroy (THREAD_ENTRY *thread_p, const VFID &oos_vfid);
extern int oos_insert (THREAD_ENTRY *thread_p, const VFID &oos_vfid, RECDES &recdes, OID &oid);
extern int oos_read (THREAD_ENTRY *thread_p, const OID &oid, RECDES &recdes);
extern int oos_delete (THREAD_ENTRY *thread_p, const VFID &oos_vfid, const OID &oid);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[API] oos_vfid 파라미터 용도

현재 oos_delete 구현 내에서 oos_vfid는 직접 사용되지 않는다.

추후 확장 가능성을 위해 시그니처에 포함하였다 (예: 특정 VFID에 속한 페이지임을 검증하거나, 파일 레벨 통계 업데이트 등).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 코멘트 이후 수정 반영됨:

oos_log_delete_physicalVFID *vfid_p 파라미터를 추가하고, oos_delete 호출부에서 const_cast<VFID *>(&oos_vfid)를 전달하도록 변경.

이제 oos_vfid가 실제로 WAL 로그에 기록되며, oos_log_insert_physical과 대칭적인 구조가 됨.


// Peek the header of the first chunk to find the next chunk OID
OOS_RECORD_HEADER head_header{};
err = peek_oos_header (head_oid, head_header);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[테스트 설계] 삭제 전에 next_chunk_oid를 미리 확보

oos_delete 호출 후에는 청크가 이미 삭제되어 헤더를 읽을 수 없다.

따라서 삭제 전에 peek_oos_headernext_chunk_oid를 먼저 가져와 두고, 삭제 후 두 페이지(head, next)의 free space 변화를 각각 검증한다.

err = oos_delete (thread_p, oos_vfid, target_oid);
ASSERT_EQ (err, NO_ERROR);

int free_after_delete = get_free_space_of_oid_page (target_oid);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[주의] spage_delete 후 free space가 완전히 복원되지 않는 이유

spage_insert는 레코드 데이터 + 슬롯 엔트리(SPAGE_SLOT, 4 bytes)를 함께 소비한다.

반면 spage_delete는 레코드 데이터만 total_free에 반환하고, 슬롯 엔트리는 REC_DELETED_WILL_REUSE 상태의 tombstone으로 남겨 재사용 대기 상태로 전환한다.

따라서 이 테스트에서는 "레코드 데이터 크기만큼 복구되었는가"를 검증하며, 완전한 free space 복원은 spage_compact 이후에 가능하다.

@vimkim vimkim requested review from a team, H2SU, InChiJun, YeunjunLee, hgryoo, hornetmj, hyahong and youngjun9072 and removed request for a team March 17, 2026 11:21
@vimkim
Copy link
Contributor Author

vimkim commented Mar 23, 2026

PR #6909 코드 리뷰: oos_delete API 구현

전체 요약

OOS(Out-Of-Slot) 레코드의 물리적 삭제 API를 구현한 PR입니다. 변경은 4개 파일, +478줄이며 핵심은 oos_delete() 함수와 WAL 로깅 함수 oos_log_delete_physical(), 그리고 6개의 단위 테스트입니다.


1. oos_delete — 핵심 로직 분석

동작 원리

oos_delete(thread_p, oos_vfid, oid)
  │
  ├─ current_oid = oid (head chunk)
  │
  └─ while (current_oid.pageid != NULL_PAGEID)
       ├─ pgbuf_fix(WRITE latch)     ← 페이지를 exclusive lock으로 고정
       ├─ spage_get_record(PEEK)     ← 슬롯에서 레코드를 zero-copy로 읽음
       ├─ header에서 next_chunk_oid 추출
       ├─ oos_log_delete_physical()  ← WAL: undo 데이터(삭제 전 레코드) 기록
       ├─ spage_delete()             ← 슬롯을 물리적으로 삭제, free space 회수
       ├─ pgbuf_set_dirty()          ← 더티 마킹
       └─ current_oid = next_chunk_oid (다음 chunk으로 이동)

왜 동작하는가:

  • OOS 레코드는 linked list 구조. 각 chunk의 OOS_RECORD_HEADER.next_chunk_oid가 다음 chunk을 가리키고, 마지막 chunk은 NULL_PAGEID를 가짐.
  • spage_get_record(PEEK)로 읽은 데이터에서 header만 복사하여 next 포인터를 삭제 전에 확보. 이후 spage_delete로 현재 슬롯을 삭제해도 next 정보는 이미 로컬 변수에 있으므로 chain 순회가 안전.
  • scope_exit로 페이지 unfix를 보장하여, 에러 리턴 시에도 리소스 누수가 없음.

2. WAL 로깅 (oos_log_delete_physical)

log_append_undoredo_recdes(thread_p, RVOOS_DELETE, &log_addr, recdes_p, NULL);
//                                                  undo=recdes  redo=NULL
undo (rollback) redo (crash recovery)
RVOOS_INSERT undo=NULL, redo=recdes undo→delete, redo→insert
RVOOS_DELETE undo=recdes, redo=NULL undo→insert, redo→delete

recovery.c의 등록 테이블:

{RVOOS_DELETE, "RVOOS_DELETE", oos_rv_redo_insert, oos_rv_redo_delete, NULL, NULL}
  • Redo (크래시 복구): oos_rv_redo_deletespage_delete 수행 — 커밋된 삭제를 재적용
  • Undo (롤백): oos_rv_redo_insertspage_insert_for_recovery로 레코드 복원

INSERT와 DELETE가 정확히 역연산 관계이므로 recovery 함수를 교차 사용하는 것이 올바름.


3. 발견 사항

[Medium] oos_vfid 파라미터 미사용

oos_delete 시그니처에 const VFID &oos_vfid가 있지만, 함수 본문에서 전혀 사용되지 않음. oos_log_delete_physical에서도 log_addr.vfid = NULL로 설정.

비교: oos_log_insert_physical에서는 log_addr.vfid = vfid_p로 VFID를 설정.

확인 필요: DELETE 로깅에서 vfid가 NULL인 것이 의도적인지? INSERT와 대칭적으로 VFID를 기록해야 recovery나 replication에서 필요할 수 있음. 의도적으로 불필요하다면 코멘트가 있으면 좋겠음.

[Low] 멀티 chunk 삭제 시 atomicity

각 chunk을 개별적으로 fix→log→delete→unfix. 중간 chunk 삭제 후 크래시 시:

  • 커밋 전 크래시: 트랜잭션 rollback 시 undo 로그로 각 chunk 복원 → 안전
  • 커밋 후 크래시: redo 로그로 남은 chunk들도 삭제 → 안전

WAL의 undo/redo가 chunk 단위로 기록되므로 정확히 동작.

[Info] 테스트 커버리지 — 양호

테스트 검증 내용
OosDeleteBasic 단일 chunk 삭제 후 free space 증가
OosDeleteThenReadFails 삭제 후 read 실패 확인
OosDeleteMultiChunk 2-chunk 레코드의 양쪽 페이지 free space 회수
OosUpdatePattern UPDATE 시나리오 (insert new → delete old)
OosDeleteRestoresFreeSpace free space 정밀 비교 (slot tombstone 고려)
OosDeleteLarge160KBMultiChunk 160KB 대형 레코드 (다수 chunk) 삭제

4. 결론

코드 품질이 높고, 기존 oos_insert/oos_read와 일관된 패턴을 따름. WAL 로깅의 undo/redo 대칭성이 정확하고, scope_exit를 통한 리소스 관리도 깔끔.

주요 확인 필요 사항:

  1. log_addr.vfid = NULL — DELETE에서 VFID를 기록하지 않는 것이 의도적인지 (INSERT와의 비대칭)
  2. oos_vfid 파라미터가 미사용인 채로 남아도 되는지 (향후 확장용?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant