Skip to content

[CBRD-27401] Add ALTER INDEX ... COMPACT for online overflow OID chain compaction - #7899

Open
shparkcubrid wants to merge 4 commits into
CUBRID:developfrom
shparkcubrid:CBRD-27401
Open

[CBRD-27401] Add ALTER INDEX ... COMPACT for online overflow OID chain compaction#7899
shparkcubrid wants to merge 4 commits into
CUBRID:developfrom
shparkcubrid:CBRD-27401

Conversation

@shparkcubrid

Copy link
Copy Markdown
Contributor

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

Purpose

CBRD-24094(#7447)로 비유니크 인덱스의 오버플로 OID 체인이 OID 정렬 + 디렉터리 구조로 바뀌면서, 만원인 데이터 페이지가 반반 split 되어 충전율이 50~100% 사이가 될 수 있습니다. 시리얼 삽입(OID 단조 증가)은 rightmost 채움이라 legacy와 같지만, reuse_oid 테이블에서 삭제·재삽입이 반복되어 재사용 OID가 흩어져 들어오면 오버플로 페이지가 최대 2배까지 늘 수 있습니다(CBRD-27324 코멘트 실측 1.7배). legacy의 first-fit은 100% 충전이었으므로 11.5 업그레이드 후 "중복이 많은 인덱스 크기가 커졌다"는 문의가 나올 수 있습니다.

현재 유일한 해소 수단인 ALTER INDEX … REBUILD는 테이블을 잠그고 전체를 정렬·재적재하므로 대형 테이블에서 부담이 큽니다. CREATE INDEX … WITH ONLINE은 잠금은 없으나 행 단위 삽입이라 더 느리고 로그를 많이 씁니다. 리빌드보다 가벼운, 오버플로 체인만 정리하는 온라인 컴팩션 명령을 추가합니다. (Oracle ALTER INDEX … COALESCE, SQL Server ALTER INDEX … REORGANIZE 대응)

Implementation

구문: ALTER INDEX index_name ON table_name COMPACT [WITH FILL_FACTOR = n] (n = 50~100, 기본 90). 100%로 채우지 않는 이유는 직후 랜덤 삽입에서 바로 split이 재발하는 것을 막기 위함입니다.

핵심 연산btree_ovf_compact_pull() (src/storage/btree.c): split의 역연산입니다. 체인을 왼쪽부터 훑으며 각 데이터 페이지를 목표 충전율까지 오른쪽 이웃에서 채웁니다. 양쪽 모두 OID 정렬이고 left < right이므로 이웃의 앞쪽 객체를 왼쪽 레코드 뒤에 이어붙이면 정렬이 유지됩니다.

  • 이웃의 객체가 전부 들어가면 병합: 이웃을 체인에서 unlink(btree_ovf_dir_write_header) → file_dealloc → 디렉터리 엔트리 삭제(btree_ovf_dir_remove_entry). separator가 하한이므로 엔트리 하나를 지우면 왼쪽 페이지 구간이 자동 확장되어 디렉터리 재작성이 불필요합니다.
  • 앞쪽 일부만 들어가면 재분배: 남은 객체는 그대로 두고 이웃의 separator를 새 첫 OID로 올립니다(btree_ovf_dir_set_separator, 신규). 갱신 값은 (왼쪽 페이지 최대 OID, 이 페이지 최소 OID] 이내라 디렉터리 정렬과 라우팅이 유지됩니다. 동일 OID 런(vacuum 안 된 재사용 OID 중복)은 경계에서 쪼개지 않도록 뒤로 물러섭니다. 헤드 디렉터리 페이지의 엔트리 0은 캐치올(−∞ 취급)이므로 separator를 올리지 않고 왼쪽 이웃으로만 쓰입니다.

온라인성: 병합/재분배 1회 = 시스템 연산(sysop) 1개, 해당 키의 리프 WRITE 래치 아래에서 수행 — 삽입·삭제·vacuum과 동일한 직렬화 규약입니다. 로깅은 기존 RVBT_RECORD_MODIFY_UNDOREDO(UPDATE_ALL / UPDATE_PARTIAL)만 사용하므로 신규 복구 핸들러가 없습니다. 물리 배치만 바뀌므로 sysop은 호출 트랜잭션과 무관하게 즉시 commit 합니다. 작업이 있었던 키마다 리프 래치를 놓고 키로 위치를 다시 찾으므로(btree_compact_fix_leaf) 동시 DML이 컴팩션 중인 키 외에는 막히지 않고, 클래스는 AU_FETCH_READ(SCH_S)만 잡습니다. 인터럽트 체크 포함.

동반 변경 — 키 중간 스캔 재개를 OID 기준으로(btree_ovf_scan_locate_resume()): 이 부분이 이번 작업에서 가장 주의가 필요했던 지점입니다. btree_range_scan은 한 키의 객체가 스캔 OID 버퍼(기본 4페이지 = 8,192개)보다 많으면 키 중간에서 중단하고, 재개 위치를 "마지막으로 가시 객체가 있던 오버플로 페이지"로 저장합니다. 이 방식은 "가시 객체가 있는 페이지는 반납되지 않는다"는 전제(코드 주석의 3번)에 의존하는데, 컴팩션은 살아 있는 객체를 옮기고 페이지를 반납하므로 그 전제를 깹니다. 그대로 두면 중단된 스캔이 반납된 페이지를 fix 하려다 실패하거나, 이미 지나간 페이지로 옮겨진 객체를 건너뛰어 행이 조용히 누락됩니다. CBRD-24094로 체인이 전역 OID 정렬이 되었으므로, 재개 위치를 페이지가 아니라 마지막으로 처리한 OID로 바꿨습니다. 재개 시 디렉터리로 라우팅해 해당 페이지를 찾고 페이지 내에서 그 OID보다 큰 첫 객체부터 처리합니다. 페이지가 병합·분할·반납되어도 정확하며, 삽입 split으로 인한 중복 반환 가능성도 함께 없어집니다. legacy(유니크) 체인은 OID 정렬이 아니므로 기존 페이지 기준 재개를 그대로 씁니다(컴팩션 대상도 아님).

대상 제외(no-op, 오류 아님): 유니크 인덱스(legacy 체인), 1페이지 이하 체인, 이미 충전율 이상인 페이지. 파티션 테이블은 각 파티션의 인덱스를 순회합니다.

배선: PT_COMPACT_INDEX + COMPACT/FILL_FACTOR 토큰(둘 다 식별자로도 계속 사용 가능) → do_alter_index_compact()(execute_schema.c) → NET_SERVER_BTREE_COMPACT_OVERFLOWxbtree_compact_overflow().

Remarks

신규 기능이며 기존 ALTER INDEX … REBUILD, SHOW INDEX CAPACITY 동작 변경은 없습니다. 스캔 재개 방식 변경은 비유니크 인덱스에서 한 키가 스캔 버퍼를 넘을 때만 타는 경로입니다.

진단 기준(매뉴얼 예정): SHOW INDEX CAPACITY OF tbl.idxTotal_free_space_ovf / (Total_used_space_ovf + Total_free_space_ovf)가 30% 이상이고 Num_ovf_page가 의미 있는 크기일 때 COMPACT 권고, 실행 후 같은 명령으로 확인.

실측 1 — 긴 체인(3키 × 30,000행 reuse_oid, 절반 삭제 + vacuum 후 재사용 OID로 재삽입):

항목 컴팩션 전 COMPACT 후
Num_ovf_page 365 144
Total_free_space_ovf 3.6M 184.9K
평균 충전율 37% 94% (fill_factor 95 지정)

실측 2 — 짧은 체인(200키 × 약 2,250행, 키당 데이터가 2.3페이지분): 800 → 600 페이지. 키당 이론 최소치(3페이지)와 일치하며, 더 채울 수 없어 fill_factor = 100으로도 동일합니다. 즉 짧은 체인은 페이지 단위 꼬리 때문에 감소율이 낮습니다.

실행 시간(실측 2와 같은 인덱스): COMPACT 0.07초 vs 같은 인덱스 REBUILD 5.94초. 체인이 이미 정리된 상태에서의 재실행은 변화 없음(멱등).

검증 결과

  • 전후 SELECT count(*), 키별 카운트 전부 동일, cubrid checkdb --check-btree / --check-btree-entries rc=0.
  • 유니크 인덱스: 변경 없이 정상 종료. 파티션 테이블(hash 4): 파티션별 72 → 47 페이지, 카운트 동일, checkdb rc=0.
  • 문법·오류 경로: FILL_FACTOR = 49 / 101 → "FILL_FACTOR argument must be between 50 and 100.", 없는 인덱스/클래스 → 기존 에러. compact, fill_factor를 테이블명·컬럼명으로 사용 가능함을 확인(예약어화 아님).
  • 동시성: 3세션 INSERT/DELETE/SELECT 진행 중 COMPACT 0.11초 완료, 세션 오류 0, 인덱스 스캔 카운트와 테이블 카운트 일치.
  • 키 중간 재개 레이스: 30,000중복 키(스캔 중단이 스캔당 3~4회 발생)에 대해 컴팩션 1,212회를 스캔 80회와 경쟁시켜 매 스캔이 정확히 30,000행 반환, count(distinct)도 동일(누락·중복 0), checkdb rc=0.
  • 컴팩션 중 kill -9 후 재기동: 복구 성공, 키별 카운트 동일, checkdb rc=0, 재실행으로 컴팩션 정상 완료(아래 "복구" 항목).

후속: vacuum 시 자동 병합(InnoDB MERGE_THRESHOLD 방식)은 vacuum 경로에 쓰기가 추가되어 리스크가 크므로 이번 범위에서 제외하고 별도 이슈로 검토합니다. 매뉴얼(ALTER INDEX 절, 인덱스 크기 증가 시 조치)과 11.5 릴리스 노트 항목, 테스트케이스(TC PR)는 별도로 진행합니다.

🤖 Generated with Claude Code

…n compaction

CBRD-24094 (CUBRID#7447) turned the overflow OID chains of non-unique indexes into
OID-ordered chains with a separator directory. A full data page now splits in
half, so a chain that receives scattered (reused) OIDs can end up 50~100% full,
up to ~2x the legacy first-fit size. The only remedy so far was
ALTER INDEX ... REBUILD, which locks the table and sorts/reloads everything.

Add ALTER INDEX idx ON tbl COMPACT [WITH FILL_FACTOR = n] (n = 50..100,
default 90): walk the leaf level and, for every key with a directory overflow
chain, fill each data page up to the fill factor from its right neighbor by
appending the neighbor's lowest objects to it (both are OID-ordered, so the
result stays ordered). When all of the neighbor's objects fit, the neighbor is
merged away: unlinked from the chain, its directory entry dropped and the page
deallocated -- separators are lower bounds, so removing an entry extends the
left page's range without rewriting the directory. When only a prefix fits, the
neighbor keeps the rest and its separator is raised to its new first OID.

Each merge is one system operation under the key's leaf write latch, logged with
the generic RVBT_RECORD_MODIFY_UNDOREDO record modifications (no new recovery
handler), and committed independently of the outer transaction since only the
physical layout changes. The leaf latch is released after every key that had
work done and the position is re-located by key, so concurrent DML is not
blocked beyond the key being compacted; the class is held under SCH_S only.
Unique indexes (legacy chains), single-page chains and pages already above the
fill factor are no-ops. Partitioned classes compact every partition's index.

Compaction also required the mid-key range scan resume to change. A scan that
interrupts inside one key (more objects than the scan OID buffer holds) saved
its position as the last overflow page that had a visible object, which relies
on such a page never being deallocated. Compaction moves live objects and frees
pages, so the saved page can disappear or lose objects to an already processed
page, silently skipping rows. Since CBRD-24094 chains are globally OID-ordered,
the resume point is now the last OID processed: on resume the chain is routed by
that OID through the directory and processing continues with the first greater
object. This is correct across merges, splits and page frees, and also removes
the duplicate-row exposure a concurrent split had. Legacy (unique) chains are
not OID-ordered and keep the page-based resume; they are not compacted either.

New pieces: xbtree_compact_overflow () and helpers in btree.c, the
NET_SERVER_BTREE_COMPACT_OVERFLOW request, PT_COMPACT_INDEX with the COMPACT /
FILL_FACTOR keywords (both remain usable as identifiers), and
do_alter_index_compact () in execute_schema.c.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@shparkcubrid

Copy link
Copy Markdown
Contributor Author

/run all

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

❌ TC Merge Gate — Merge Blocked

One or more TC PRs are still open. Please merge or close them before merging this PR.

TC Repositories & Branches:

  • cubrid-testcases: TC PR tc/pr-7899 is open (draft) — must be merged or closed first
  • cubrid-testcases-private-ex: TC PR tc/pr-7899 is open (draft) — must be merged or closed first

Steps to unblock:

  1. Merge or close all TC PRs listed above.
  2. Re-run this check: Actions tab → TC Merge Gate → Re-run failed jobs

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

🧪 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.

@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Security Review

새 compaction RPC가 client-supplied BTID를 서버 측 객체 권한 검사 없이 storage 계층으로 전달합니다. SQL 경로의 AU_INDEX 검사를 우회해 권한이 없는 인덱스에 물리 변경과 자원 부하를 유발할 수 있으므로 서버 측 authorization 검증이 필요합니다.

Reviews (1): Last reviewed commit: "[CBRD-27401] Add ALTER INDEX ... COMPACT..." | Re-trigger Greptile

Comment thread src/storage/btree.c
Comment on lines +14733 to +14744
BTREE_GET_OID (record.data + (num_objects - 1) * obj_size, &cur_oid);
if (OID_GT (&cur_oid, anchor))
{
/* Leftmost object greater than the anchor. */
idx = num_objects - 1;
min = 0;
max = num_objects - 2;
while (min <= max)
{
mid = (min + max) / 2;
BTREE_GET_OID (record.data + mid * obj_size, &cur_oid);
if (OID_GT (&cur_oid, anchor))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 동일 OID 객체 누락

동일 OID의 MVCC 객체는 여러 overflow page에 걸쳐 존재할 수 있습니다. 그러나 이 코드는 재개 시 anchor보다 큰 OID만 찾습니다. 이전 iteration에서 처리한 페이지의 마지막 OID와 같은 OID가 다음 페이지에도 남아 있으면, 이를 이미 처리된 것으로 판단하여 현재 snapshot에서 visible한 객체까지 range scan 결과에서 조용히 누락합니다. 페이지 이동에도 안전하게 재개하려면 OID뿐 아니라 처리한 physical version까지 식별하거나, 경계의 동일 OID 객체를 visibility 규칙에 맞게 다시 확인해야 합니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

맞는 지적입니다. 수정했습니다.

코드로 확인한 내용부터 정리합니다.

  • 키 중간 중단은 페이지 경계에서만 일어납니다(btree_range_scan_select_visible_oids ()는 버퍼가 부족하면 그 페이지를 아예 처리하지 않고 멈춥니다). 수정 전 앵커는 그렇게 처리된 마지막 페이지의 마지막 객체였습니다.
  • 재사용 OID의 여러 미청소 버전이 한 체인에 공존하는 상태는 btree_insert_object_ordered_by_oid ()의 주석이 명시하는 실제 상태입니다("vacuum이 heap 엔트리는 청소했지만 b-tree 엔트리는 청소하지 않은" 창). 같은 OID는 이분탐색이 일치한 위치에 삽입되므로 버전들이 인접해 런을 만듭니다.
  • 스냅샷에 보이는 버전은 런 안에서 어느 위치든 될 수 있는데(어떤 버전이 보이는지는 스냅샷이 결정), 앵커가 런의 OID인 상태에서 OID_GT로 재개하면 런 전체를 건너뜁니다. 경계가 런 안에 떨어지고 보이는 버전이 그 뒤에 있으면 조용히 누락됩니다.

수정: 앵커를 "처리 구간의 마지막 객체"에서 **"스냅샷이 받아들인 마지막 객체"**로 바꿨습니다(bts->O_last_visible_oid, btree_select_visible_object_for_range_scan ()에서 기록). 이렇게 하면 OID_GT 재개가 런과 무관하게 정확합니다. 한 OID의 버전 중 스냅샷에 보이는 것은 최대 하나이고 그것이 바로 앵커 자신이므로, 보이는 객체를 건너뛰거나 두 번 반환하는 일이 원리적으로 불가능합니다. 앵커와 옛 경계 사이의 객체들은 다시 검사되지만 모두 비가시라 그대로 걸러집니다. 리프 레코드 객체는 체인과 정렬 관계가 없으므로 체인 순회 시작 시 앵커를 초기화해 절대 앵커가 되지 않게 했습니다.

검증에 대해서는 한계를 그대로 적습니다. 이 상태는 vacuum이 heap 슬롯은 해제하고 b-tree 엔트리는 아직 지우지 않은 짧은 창에서만 생기고, vacuum을 막으면 OID 재사용 자체가 일어나지 않아(재사용 슬롯은 vacuum 이후에 나옵니다) 의도적으로 만들어내지 못했습니다. 그래서 이 수정의 근거는 재현이 아니라 위 코드 논거입니다. 대신 재개 경로 자체는 다음으로 확인했습니다.

  • reuse_oid 테이블에서 한 키에 30,000 객체(오버플로 83페이지)를 만들고 delete+reinsert 처른을 돌리며 인덱스 경로와 heap 경로를 같은 문장의 스냅샷으로 비교(k = 7k + 0 = 7): 30/30 일치, checkdb rc=0.
  • 미청소 버전을 138,000 객체(오버플로 344페이지)까지 쌓아 스캔당 중단·재개가 여러 번 일어나게 한 스트레스: 20/20 일치, checkdb rc=0.

푸시: b438be9

Comment thread src/storage/btree.c Outdated
Comment on lines +15075 to +15080
if (freed == 0)
{
/* Already compact; nothing was held for long. Keep the latch and move on. */
btree_clear_key_value (&clear_key, &key);
slot++;
continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 WRITE 래치 장기 점유

freed == 0은 변경이 없었다는 의미가 아닙니다. 재분배만 수행한 chain은 여러 sysop과 페이지 수정을 완료해도 0을 반환합니다. 이 분기는 leaf WRITE latch를 해제하거나 interrupt를 확인하지 않고 다음 key로 진행하므로, 페이지 해제 없이 재분배되는 긴 chain이나 그런 key가 한 leaf에 몰리면 해당 leaf의 DML이 전체 작업 동안 차단되고 interrupt 처리도 다음 leaf까지 지연됩니다. 실제 변경 여부를 별도로 반환해 변경된 key마다 latch를 양보하고, chain 순회에도 interrupt 지점을 두어야 합니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

맞는 지적입니다. 수정했습니다.

freed == 0을 "할 일이 없었다"로 쓴 것이 잘못이었습니다. 재분배만 일어난 체인은 sysop을 여러 번 커밋하고 페이지를 고쳐도 pages_freed가 0이라, 그 분기가 리프 WRITE 래치를 쥔 채로 다음 키로 넘어가고 인터럽트 확인도 건너뛰었습니다.

  • btree_ovf_compact_chain ()pages_freed와 별도로 work_done을 돌려주고, 병합이든 재분배든 sysop을 커밋할 때마다 참으로 설정합니다.
  • 호출부는 !work_done, 즉 체인을 읽기만 한 경우에만 래치를 유지합니다. 무엇이든 쓴 경우에는 예외 없이 래치를 놓고, 인터럽트를 확인한 뒤 키로 위치를 다시 찾습니다.
  • 체인 순회 루프 안(병합 사이)에도 인터럽트 확인을 넣었습니다. 병합 하나가 각각 하나의 시스템 연산이므로 그 지점에서 빠져나가도 체인은 일관된 상태입니다.

keys_compacted는 문서화된 의미("페이지가 하나 이상 줄어든 키 수")를 유지했습니다.

푸시: b438be9

Comment on lines +5135 to +5138
ptr = or_unpack_btid (request, &btid);
ptr = or_unpack_int (ptr, &fill_factor);

error = xbtree_compact_overflow (thread_p, &btid, fill_factor, &keys_compacted, &pages_freed);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security 인덱스 권한 검사 우회

새 RPC는 클라이언트가 전달한 BTID를 객체 권한 확인 없이 물리 compaction에 사용합니다. 인증된 사용자가 protocol request를 직접 구성하면 SQL 경로의 AU_INDEX 검사를 우회하여 권한이 없는 인덱스의 overflow page를 수정·해제하고 I/O 및 latch 부하를 유발할 수 있습니다. CHECK_DB_MODIFICATION은 transaction의 수정 가능 여부만 검사하므로, 서버에서 BTID의 소유 class/index를 조회해 AU_INDEX 권한을 검증하거나 이 요청을 신뢰된 관리 주체로 제한해야 합니다.

How this was verified: 요청 payload의 BTID가 객체 권한 검사 없이 storage compaction 함수까지 전달되고, 해당 함수도 BTID 소유 객체에 대한 권한을 확인하지 않는 경로를 확인했습니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

지적은 사실이지만 이 PR이 만든 취약점은 아니라고 판단합니다. 근거를 남깁니다.

CUBRID의 서버측 B-tree 요청은 클라이언트가 보낸 BTID를 객체 권한 검사 없이 그대로 사용하는 것이 기존 설계입니다. 권한은 클라이언트에서 강제합니다.

  • sbtree_delete_index (NET_SERVER_BTREE_DELINDEX)는 or_unpack_btidxbtree_delete_index를 호출해 인덱스 전체를 파괴하며, au_* 검사가 없습니다. NET_SERVER_BTREE_ADDINDEX/LOADINDEX, sbtree_get_statistics, sbtree_get_key_type도 동일합니다.
  • 즉 프로토콜을 직접 구성할 수 있는 사용자는 이 PR 이전에도 임의 BTID의 인덱스를 드롭할 수 있었고, 그것은 오버플로 페이지 병합보다 훨씬 파괴적입니다. 새 요청은 같은 패턴을 따르면서 파괴력은 더 낮습니다.

SQL 경로에서는 권한을 확인합니다. do_alter_index_compact ()au_fetch_class (obj, &smcls, AU_FETCH_READ, AU_INDEX)를 호출하고, 이때 잡히는 SCH_S 잠금이 트랜잭션 종료까지 유지되어 컴팩션 중 인덱스 DDL과 직렬화됩니다.

서버에서 BTID로 소유 class/index를 역조회해 권한을 검사하는 것은 이 요청 하나가 아니라 BTID를 받는 모든 요청에 걸친 프로젝트 차원의 변경이므로, 별도 이슈로 다루는 것이 맞다고 봅니다. 이 PR에서는 기존 규약을 따르겠습니다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

설명 감사합니다. 이 맥락에서는 해당 지적을 이 PR의 신규 취약점으로 유지하지 않겠습니다. 서버 측 BTID 기반 B-tree RPC가 기존부터 클라이언트 권한 검사를 신뢰하는 설계이고, NET_SERVER_BTREE_DELINDEX 등 기존 요청이 같은 경계를 가지며 더 큰 파괴력을 갖는다면, sbtree_compact_overflow의 권한 역조회 부재는 이 PR에서 새로 도입된 권한 우회가 아니라 기존 프로토콜 설계의 공통 문제입니다. SQL 경로의 do_alter_index_compact()에서 AU_INDEX 검사를 수행한다는 점도 확인 기준에 부합합니다. 이 코멘트는 철회하고, BTID를 받는 서버 요청 전반의 권한 검증은 별도 프로젝트 이슈로 분리하는 것이 적절하겠습니다.

Review follow-up on two defects.

1. The resume anchor was the last object of the last fully processed overflow
   page. A reusable OID can have several un-vacuumed versions in one chain --
   btree_insert_object_ordered_by_oid () documents the state and inserts equal
   OIDs adjacently -- and which version a snapshot sees is decided by the
   snapshot, so it can sit anywhere in that run. Resuming with OID_GT on the
   run's OID skips the whole run, silently dropping a visible object when the
   interrupt boundary falls inside one. Anchor on the last object accepted by
   the snapshot instead (bts->O_last_visible_oid, recorded in
   btree_select_visible_object_for_range_scan ()): at most one version of an
   OID is visible and it is the anchor itself, so a strictly-greater resume can
   neither skip nor repeat a visible object, and the invisible objects
   re-examined in between are filtered again. The anchor is reset when the
   chain walk starts, so a leaf record object can never become one.

2. btree_ovf_compact_chain () reported only freed pages, so a chain that was
   redistributed without freeing anything looked like "no work" to the driver,
   which then kept the leaf write latch and skipped the interrupt check while
   moving to the next key. Report work_done separately and yield the latch
   whenever anything was written; also check for interrupts between merges
   inside the chain, which is safe because each merge is its own system
   operation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shparkcubrid

Copy link
Copy Markdown
Contributor Author

/run all

Self-review follow-up, two items.

The grammar action read the int member of the value union unconditionally, but
unsigned_integer keeps a literal that does not fit an int in the bigint member
and tags the node BIGINT. Reading the int member (a long) and narrowing it to
int kept the low 32 bits, so FILL_FACTOR = 4294967386 was accepted and behaved
as 90, while 4294967345 was rejected as 49. Accept only PT_TYPE_INTEGER and
report the same range error otherwise. Note that ONLINE PARALLEL = n has the
same pattern; that one is pre-existing and left alone here.

btree_ovf_compact_pull () built the grown left record in a third page-sized
stack buffer. Append into the copy already read instead: the old image stays as
its prefix and serves as the undo image, and the right page's remainder is a
suffix of its own copy, so it is written and logged straight from there. One
page-sized buffer and two memcpy of a page each are gone, cutting the frame
from ~48KB to ~32KB, in line with the delete path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shparkcubrid

Copy link
Copy Markdown
Contributor Author

/run all

…list

NET_SERVER_BTREE_COMPACT_OVERFLOW was placed with the other b-tree requests, in
the middle of NET_SERVER_REQUEST_LIST, which renumbers every request after it.
The last four commits that added a request (CBRD-26936, CBRD-26527, CBRD-23700,
CBRD-26123) all appended at the end of the list instead, so that the numbers
already in use never move. Follow that and put the new item last.

No behaviour change within a build: the enum and the dispatch table are both
generated from this list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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