[CBRD-26983] Replicate the shutdown serial write-back and reject stale _db_serial images on an active node - #7877
Open
hgryoo wants to merge 10 commits into
Open
[CBRD-26983] Replicate the shutdown serial write-back and reject stale _db_serial images on an active node#7877hgryoo wants to merge 10 commits into
hgryoo wants to merge 10 commits into
Conversation
…at server shutdown The write-back that CUBRID#7720 added to xboot_shutdown_server runs on a borrowed system worker transaction, and log_does_allow_replication () is false for both kinds of system transaction. The write therefore carried no replication record: an HA master that stopped cleanly resumed at the last issued value while its standby kept the block end. QA saw it on the ha_repl run of sql/_13_issues/_23_1h/cases/cbrd_24544.sql - master current_val 70001, slave 1330001 - and the per-statement catalog comparison failed. Move the server's write-back into css_init's shutdown sequence, after the request workers, vacuum and load sessions have stopped and before the log writer stops, and run it on a regular transaction of its own the way the dblink 2PC daemon takes one (logtb_assign_tran_index, xtran_server_commit, logtb_free_tran_index). On a regular transaction serial_update_serial_object opens its flush-marked system operation, the path a NEXT_VALUE advance takes, so the record is written at the operation's end and the log writer ships it before the process exits. The pass in xboot_shutdown_server is now for standalone processes only, and the system worker it borrowed goes with it: outside SERVER_MODE lock_get_object_lock () returns X_LOCK, so serial_update_serial_object never opened a system operation there and the borrow was only ever for the server. Measured on a two-node cluster (Docker, debug build, AUTO_INCREMENT (1, 70000), cached_num 20, two rows issued). Standby stopped first, active node stopped with cubrid heartbeat stop, both restarted: both nodes read 70001 (was 70001 / 1330001) and the next id is 140001 (was 1400001). Single node, cubrid server stop and restart: 1, 70001, 140001. With the standby alive, heartbeat stop promotes the standby before the leaving node has written the flush, and the record arrives after the promotion; the next commit is about that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ackwards on an active node With the previous commit alone, cubrid heartbeat stop on the active node promotes the standby about two seconds later, before the leaving node's shutdown has written its serial cache back. The promoted node issued 1400001 from the block end it had (1330001) and reserved 2730001; then the old master's log tail arrived and its write-back image set cur_val to 70001. The next insert on the active node gave 140001, below a value already issued, and the sequence was on its way to issue 1400001 twice. A standby must follow every _db_serial image, so that a promotion resumes at the master's last issued value. A node that issues values itself must not: an image that reaches an active node is the leaving master's tail, applied after the promotion, and is stale by definition. serial_repl_image_is_stale () decides it for xlocator_repl_force: on an active or to-be-standby node an image that would move cur_val against the serial's direction is not applied and the object counts as applied; a cyclic serial has no direction and its row is left alone; a failure to read either record applies the image as before. The same rule covers the pre-existing case of an old master's tail carrying a lower block end than the one the promoted node has reserved since. Measured on the same cluster, three runs: after the old master's tail arrives, the promoted node keeps 2730001 and its next id is 1470001 (was 70001 and 140001); the server log records the skipped image. The standby-first order is unchanged: both nodes 70001 after the restart, next id 140001. No assert in the debug build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
❌ TC Merge Gate — Merge BlockedOne or more TC PRs are still open. Please merge or close them before merging this PR. TC Repositories & Branches:
Steps to unblock:
|
🧪 TC Test Environment ReadyCircleCI Testing:
TC Repositories & Branches:
Next Steps:
|
Member
Author
|
/run all |
Member
Author
|
/run all |
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…is empty serial_flush_cache_pool_replicated took a transaction slot on every server shutdown, even when there was nothing to write back. On a saturated transaction table the allocation fails and leaves ER_TM_TOO_MANY_CLIENTS in the server log for no reason. Return before the allocation when the cache is not initialized or holds no entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The skipped image was visible only with er_log_debug on, so an operator comparing the two nodes' _db_serial rows after a failover had nothing in the server log to explain the difference. Log it as the new ER_HA_REPL_SERIAL_IMAGE_SKIPPED at notification severity, which the default error_log_level writes, with the serial's name, this node's current_val and the image's value. The er_set is wrapped in er_stack_push / er_stack_pop so the applier's error area is left as it was and the object still counts as applied. ER_HA_GENERIC_ERROR is not reused: it is in the default call_stack_dump_activation_list, so every use would dump a call stack. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Member
Author
|
/run all |
Member
Author
|
/run all |
hgryoo
marked this pull request as ready for review
September 10, 2026 06:47
Contributor
|
해제된 heap page의 PEEK 레코드 사용과 committed serial 정의의 전체 skip 가능성이 있어 현재 상태로는 병합하기 안전하지 않습니다. Reviews (1) · Last reviewed commit: "Merge branch 'develop' into CBRD-26983-h..." |
…eback Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
serial_repl_image_is_stale took the row from xlocator_repl_force, which reads it with heap_get_visible_version (..., PEEK, ...) on the force scan cache. That scan cache is started with cache_last_fix_page false, so heap_clean_get_context unfixes the home page before the record reaches the caller, and its data points into a frame any thread may reuse. Deserializing it there can compare against whatever the frame holds and skip an image that should be applied, or apply one that should not. Read the row in serial_repl_image_is_stale instead, with the scan cache the rest of serial.c uses: heap_scancache_quick_start_with_class_oid keeps the page fixed until heap_scancache_end, so the PEEK record stays valid while the attributes are read. The applier already holds the row's X lock from the xbtree_find_unique in locator_repl_prepare_force, so the row read here is the one the update is about to replace. The fix costs a page fix only for _db_serial rows on a node that issues values itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member
Author
|
/run all |
Member
Author
|
/rerun 34451435296 |
Member
Author
|
/run rerun 34451435296 |
Member
Author
|
/run rerun 34470317942 |
Member
Author
|
/run all |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
http://jira.cubrid.org/browse/CBRD-26983
Purpose
#7720 은 serial 캐시 블록을 버리기 전에 미발급 구간을
_db_serial에 되돌려 적고, 서버 셧다운에서도 같은 일을 한다. QA 의 ha_repl 에서sql/_13_issues/_23_1h/cases/cbrd_24544.sql이 구문별 카탈로그 비교에 실패했다. master 재시작 뒤a_tbl_ai_a의current_val이 master 는 70001, slave 는 1330001 이었다.xboot_shutdown_server에서 빌려 쓴 system worker 트랜잭션으로 돈다.log_does_allow_replication ()(src/transaction/log_comm.c) 은 active worker 트랜잭션이 아니면 거짓이라,serial_update_serial_object가 flush mark 도 복제 기록도 남기지 않았다. master 만 값을 낮추고 standby 는 블록 끝을 유지했다.cubrid heartbeat stop을 걸면 약 2초 뒤 standby 가 승격되는데, 종료 중인 노드는 그때 아직 flush 를 쓰지 않았다. 이미지는 이미 active 가 되어 값을 발급한 노드에 도착하고, 적용하면current_val이 2730001 에서 70001 로 내려간다. 다음 INSERT 는 이미 발급된 1400001 보다 낮은 140001 을 받았고, 그대로 두면 1400001 이 두 번 나온다.그래서 함께 넣어야 하는 두 변경이다. 복제되는 트랜잭션 위의 셧다운 write-back, 그리고 active 노드는 복제된
_db_serial이미지로current_val을 뒤로 돌리지 않는다는 수신 규칙.Implementation
src/connection/server_support.c
css_init의 셧다운 순서에서 요청 워커, vacuum, load 세션이 멈춘 뒤, log writer 를 멈추기 전에 새serial_flush_cache_pool_replicated를 부른다. 이 시점에는 값을 발급하는 주체가 없고 log writer 는 아직 기록을 보낼 수 있다.src/query/serial.c, src/query/serial.h
serial_flush_cache_pool_replicated는 dblink 2PC 데몬처럼 일반 트랜잭션을 할당받아 (logtb_assign_tran_index,xtran_server_commit,logtb_free_tran_index) 그 위에서serial_flush_cache_pool을 돈다. 각 쓰기는NEXT_VALUE가 값을 올릴 때와 같은 경로인serial_update_serial_object의 flush mark 가 붙은 system operation 을 지나고, applier 는 그 operation 의 끝에서 적용한다.serial_repl_image_is_stale은 applier 가 가져온_db_serial이미지를 이 노드에 적용하지 말아야 하는지 판정한다. active 또는 to-be-standby 노드에서 serial 의 진행 방향과 반대로current_val을 움직이는 이미지는 적용하지 않는다. standby 는 모든 이미지를 적용한다. cyclic serial 은 방향이 없어 active 노드에서는 행을 건드리지 않는다. 두 레코드 중 하나라도 읽지 못하면 이전처럼 적용한다. 건너뛴 이미지는 새 알림ER_HA_REPL_SERIAL_IMAGE_SKIPPED로 서버 로그에 남긴다. serial 이름, 이 노드의current_val, 이미지의 값을 적고,er_stack_push/er_stack_pop으로 감싸 applier 의 오류 영역은 그대로 둔다. 기본error_log_level에서 기록되므로 두 노드의 값이 다른 이유가 운영 로그에 남는다.serial_flush_cache_pool_replicated는 캐시에 엔트리가 없으면 트랜잭션을 할당하지 않는다. 트랜잭션 테이블이 꽉 찬 종료에서 쓸 것도 없이ER_TM_TOO_MANY_CLIENTS를 남기지 않기 위해서다.src/base/error_code.h, msg/en_US.utf8/cubrid.msg, msg/ko_KR.utf8/cubrid.msg
ER_HA_REPL_SERIAL_IMAGE_SKIPPED와 그 메시지를 추가한다. 기존ER_HA_GENERIC_ERROR는 기본call_stack_dump_activation_list에 들어 있어 쓸 때마다 콜스택이 덤프되므로 재사용하지 않는다.src/transaction/locator_sr.c
xlocator_repl_force는locator_update_force앞에서serial_repl_image_is_stale로 판정해, stale 이면 행을 건드리지 않은 채 그 객체를 적용된 것으로 처리한다.src/transaction/boot_sr.c
xboot_shutdown_server의 write-back pass 는SA_MODE전용이 되고 빌려 쓰던 system worker 도 없어진다.SERVER_MODE밖에서는lock_get_object_lock ()이X_LOCK을 돌려serial_update_serial_object가 system operation 을 열지 않는다.Verification
디버그 빌드, Docker 2노드 HA (
ha_mode=on,ha_copy_sync_mode=sync:sync), QA 시나리오 그대로.CREATE TABLE a_tbl (a INT AUTO_INCREMENT (1, 70000), b INT),auto_increment_cache_size = 20, 2건 INSERT (id 1, 70001, 두 노드 블록 끝 1330001),cubrid heartbeat stop으로 정상 종료. 두 순서를 다섯 번 실행했고 (#7720 브랜치에서 셋, 이 브랜치에서 둘, 뒤의 하나는 develop 머지 후) 값이 모두 같다.값은 두 가지 재개 지점에서 나온다. 마지막 발급값 70001 에서 재개하면 다음 id 는 140001 이고 새 블록 끝은 1470001 이다. 블록 끝 1330001 에서 재개하면 다음 id 는 1400001 이고 새 블록 끝은 2730001 이다. 표의 세 열은 develop, 첫 커밋만 넣은 상태, 두 커밋을 넣은 이 PR 이다.
cubrid server stop뒤 재시작: id 1, 70001, 140001. 마지막 정지 뒤current_val140001.csql -S로 2건 발급 후 종료: 다음csql -S가current_val70001 을 읽고 (이전에는 블록 끝) 140001 을 발급한다.xboot_shutdown_server의SA_MODEpass 이며 system worker 를 빌리지 않는다.HA repl info: ... current_val 2730001, image 70001). 옛 master 의 로그에는 없다. 단일 노드와 standalone 도 같다.Remarks
heartbeat stop이 바로 승격하는 경우도 위의 이유로 같다. standby 인 채로 write-back 을 적용한 노드는 마지막 발급값에서 재개한다.csql -S,loaddb) 의 serial 쓰기는 전후 모두 복제되지 않는다.SA_MODE에서log_does_allow_replication ()은 거짓이다.boot_sr.c의 정리는 분리 가능하다. 서버의 pass 가css_init으로 옮겨져 그 자리에서는 쓸 것이 없어졌기 때문에 함께 넣었다.cbrd_24544.sqlha_repl 실행이 재현이며, 이 브랜치에서 재실행을 요청한다.