Skip to content

[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
CUBRID:developfrom
hgryoo:CBRD-26983-ha-writeback
Open

[CBRD-26983] Replicate the shutdown serial write-back and reject stale _db_serial images on an active node#7877
hgryoo wants to merge 10 commits into
CUBRID:developfrom
hgryoo:CBRD-26983-ha-writeback

Conversation

@hgryoo

@hgryoo hgryoo commented Sep 7, 2026

Copy link
Copy Markdown
Member

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_acurrent_val 이 master 는 70001, slave 는 1330001 이었다.

  • 셧다운 write-back 은 xboot_shutdown_server 에서 빌려 쓴 system worker 트랜잭션으로 돈다. log_does_allow_replication () (src/transaction/log_comm.c) 은 active worker 트랜잭션이 아니면 거짓이라, serial_update_serial_object 가 flush mark 도 복제 기록도 남기지 않았다. master 만 값을 낮추고 standby 는 블록 끝을 유지했다.
  • decache 의 write-back 은 DDL 자신의 트랜잭션에서 쓰기 잠금 전에 돌아 flush mark 가 붙은 system operation 을 지난다. DDL 이 롤백되어도 복제되므로 영향이 없다.
  • 셧다운 write-back 을 복제되게 하자 두 번째 문제가 드러났다. active 노드에 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_forcelocator_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 이다.

순서 노드 develop (#7720) 셧다운 flush 복제만 이 PR
standby 먼저 정지, active 정지, 둘 재시작 active 70001, 다음 id 140001 같음 같음
standby 1330001 70001 70001
standby 가 살아 있는 채 active 정지, standby 승격 (1330001 에서 1400001 발급, 2730001 예약), 옛 master 의 남은 로그가 승격 뒤 도착 승격 노드 남은 로그에 write-back 이 없어 2730001 유지, 다음 id 1470001 이미지가 적용되어 70001 로 내려감, 다음 id 140001 (이미 발급된 1400001 보다 낮다) 이미지를 거부해 2730001 유지 (건너뜀 로그), 다음 id 1470001
  • standby 의 값은 applier 가 따라잡은 뒤의 것이다. 재시작 직후를 찍으면 아직 이전 값이 보일 수 있다. 다섯 번째 실행에서 실제로 그랬고, 폴링에서 1330001 다음에 write-back 의 70001, 그다음 새 블록 끝 1470001 이 로그 순서대로 들어오는 것을 확인했다.
  • 두 번째 순서에서 이 PR 의 숫자는 develop 과 같다. 승격이 write-back 보다 먼저이므로 gap 은 오늘과 같이 남고, 수신 규칙이 하는 일은 첫 커밋만으로는 생기는 역행을 막는 것이다.
  • 단일 노드, cubrid server stop 뒤 재시작: id 1, 70001, 140001. 마지막 정지 뒤 current_val 140001.
  • standalone, csql -S 로 2건 발급 후 종료: 다음 csql -Scurrent_val 70001 을 읽고 (이전에는 블록 끝) 140001 을 발급한다. xboot_shutdown_serverSA_MODE pass 이며 system worker 를 빌리지 않는다.
  • 모든 실행에서 서버 로그에 assert, abort, FATAL 없음.
  • 리뷰 반영 세 커밋 뒤 같은 두 순서를 다시 실행했다. 값은 같고, 승격 노드의 서버 로그에 거부 알림이 한 줄 남는다 (HA repl info: ... current_val 2730001, image 70001). 옛 master 의 로그에는 없다. 단일 노드와 standalone 도 같다.

Remarks

  • 수신 규칙은 active 노드의 뒤로 가는 갱신에만 적용된다. standby 의 카탈로그는 여전히 master 의 행을 그대로 따르고, ha_repl 이 비교하는 것은 그것이다.
  • 수신 규칙은 이 PR 의 write-back 에만 걸리는 것이 아니다. 옛 master 가 남긴 이미지가 승격 노드의 예약보다 낮은 블록 끝인 경우도 이제 적용되지 않는다. 이전에는 적용되어 같은 역행이 가능했다.
  • 비정상 종료 뒤의 failover 는 이전처럼 블록 끝 너머에서 재개한다. 복제할 write-back 이 없다. 정상 종료 직후 heartbeat stop 이 바로 승격하는 경우도 위의 이유로 같다. standby 인 채로 write-back 을 적용한 노드는 마지막 발급값에서 재개한다.
  • standalone 프로세스 (csql -S, loaddb) 의 serial 쓰기는 전후 모두 복제되지 않는다. SA_MODE 에서 log_does_allow_replication () 은 거짓이다.
  • cyclic serial, 감소 serial, to-be-standby 상태의 분기는 코드 판독으로만 확인했다. release 빌드는 CI 에 맡긴다.
  • boot_sr.c 의 정리는 분리 가능하다. 서버의 pass 가 css_init 으로 옮겨져 그 자리에서는 쓸 것이 없어졌기 때문에 함께 넣었다.
  • 테스트케이스 변경 없음. QA 의 cbrd_24544.sql ha_repl 실행이 재현이며, 이 브랜치에서 재실행을 요청한다.
  • TC 게이트, CI, QA 재실행이 끝날 때까지 draft.

hgryoo and others added 2 commits September 7, 2026 13:45
…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>
@github-actions

github-actions Bot commented Sep 7, 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-7877 is open (draft) — must be merged or closed first
  • cubrid-testcases-private-ex: TC PR tc/pr-7877 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 7, 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.

@hgryoo hgryoo self-assigned this Sep 7, 2026
@hgryoo

hgryoo commented Sep 7, 2026

Copy link
Copy Markdown
Member Author

/run all

@hgryoo hgryoo changed the title [CBRD-26983] Replicate the serial cache write-back at server shutdown and keep an active node from applying a stale one [CBRD-26983] Replicate the shutdown serial write-back and reject stale _db_serial images on an active node Sep 7, 2026
@hgryoo

hgryoo commented Sep 7, 2026

Copy link
Copy Markdown
Member Author

/run all

hgryoo and others added 3 commits September 9, 2026 13:10
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>
@hgryoo

hgryoo commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

/run all

@hgryoo

hgryoo commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

/run all

@hgryoo
hgryoo marked this pull request as ready for review September 10, 2026 06:47
@greptile-apps

greptile-apps Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Retrigger

해제된 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>
Comment thread src/transaction/locator_sr.c Outdated
Comment thread src/transaction/locator_sr.c Outdated
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>
@hgryoo

hgryoo commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

/run all

@hgryoo

hgryoo commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

/rerun 34451435296

@hgryoo

hgryoo commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

/run rerun 34451435296

@hgryoo

hgryoo commented Sep 11, 2026

Copy link
Copy Markdown
Member Author

/run rerun 34470317942

@hgryoo

hgryoo commented Sep 11, 2026

Copy link
Copy Markdown
Member Author

/run all

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