@@ -1247,7 +1247,7 @@ rollback_current_primary_switchover_guard() {
12471247fence_current_primary_after_uncertain_dcs () {
12481248 # H2 split-brain guard for the Stage-3 (DCS switchover) failure path.
12491249 #
1250- # syncerctl_switchover is invoked with --force and wrapped by timeout(1). A
1250+ # syncerctl_switchover is wrapped by timeout(1). A
12511251 # non-zero or timeout(1) (rc 124/125/137) return does NOT prove the DCS
12521252 # switchover record was never persisted: syncer can accept and persist the
12531253 # switchover request, begin asynchronously promoting the candidate, and then
@@ -1643,31 +1643,25 @@ syncerctl_switchover() {
16431643 local output
16441644 local rc
16451645 local using_timeout=0
1646-
1647- # alpha.79 v2 (Helen TL autopilot 22:31 westonnnn `442a5d2e`): pass --force
1648- # so syncer overrides any leftover "previous switchover unfinished" DCS
1649- # record from a prior successful switchover. n1ad same-cluster repeat axis
1650- # (2026-05-14 ~14:54 UTC) exposed this: after the first GREEN switchover
1651- # cleared chart-side state and OpsRequest reached Succeed, syncer's DCS
1652- # record stayed in "unfinished" state. Repeat switchovers got
1653- # `Create switchover failed: there is another switchover
1654- # maria-5d-n1ad-mariadb-switchover unfinished`. Using --force on every
1655- # invocation is safe: first-time switchovers have no leftover record so
1656- # --force is a no-op; repeated switchovers proceed cleanly. This is a
1657- # syncer-layer cleanup gap (syncer should mark its own DCS record done
1658- # when the switchover protocol completes); --force is a chart-side
1659- # workaround until syncer side cleans up properly.
1646+ SWITCHOVER_SYNCERCTL_OUTCOME=" "
1647+
1648+ # r25 completion contract: the addon never preempts an existing DCS
1649+ # switchover. `--force` used to delete an unfinished marker without proving
1650+ # that the previous database/lease/grant side effects had rolled back. A
1651+ # second request must instead receive syncer's stable
1652+ # SWITCHOVER_IN_PROGRESS conflict and retry only after the first operation
1653+ # reaches terminal truth.
16601654 if [ -n " ${stage_budget} " ] && [ " ${SWITCHOVER_HAS_TIMEOUT} " = " 1" ]; then
16611655 local wall=" ${SYNCERCTL_PER_CALL_TIMEOUT_SECONDS} "
16621656 if [ " ${stage_budget} " -lt " ${wall} " ]; then wall=" ${stage_budget} " ; fi
16631657 if [ " ${wall} " -lt 1 ]; then wall=1; fi
16641658 using_timeout=1
16651659 output=$( timeout " ${wall} " " ${SYNCERCTL_BIN} " --host " ${SYNCERCTL_HOST} " --port " ${SYNCERCTL_PORT} " \
1666- switchover --force -- primary " ${current_name} " --candidate " ${candidate_name} " 2>&1 )
1660+ switchover --primary " ${current_name} " --candidate " ${candidate_name} " 2>&1 )
16671661 rc=$?
16681662 else
16691663 output=$( " ${SYNCERCTL_BIN} " --host " ${SYNCERCTL_HOST} " --port " ${SYNCERCTL_PORT} " \
1670- switchover --force -- primary " ${current_name} " --candidate " ${candidate_name} " 2>&1 )
1664+ switchover --primary " ${current_name} " --candidate " ${candidate_name} " 2>&1 )
16711665 rc=$?
16721666 fi
16731667
@@ -1687,6 +1681,14 @@ syncerctl_switchover() {
16871681 esac
16881682 fi
16891683
1684+ case " ${output} " in
1685+ * " SWITCHOVER_IN_PROGRESS" * )
1686+ SWITCHOVER_SYNCERCTL_OUTCOME=" retryable_conflict"
1687+ log_switchover_error " Switchover failed: outcome=retryable_conflict mutation=0 reason=switchover_in_progress detail=${output} ; retry after the active DCS operation reaches terminal truth"
1688+ return 1
1689+ ;;
1690+ esac
1691+
16901692 if [ " ${rc} " -ne 0 ]; then
16911693 log_switchover_error " Switchover failed: syncerctl exited with rc=${rc} "
16921694 return 1
@@ -2307,6 +2309,14 @@ run_switchover() {
23072309 log_switchover_info " Switchover stage dcs budget=${dcs_budget} s remaining_before=$( remaining_action_budget) s primary=${current_name} candidate=${candidate_name} "
23082310 log_switchover_info " Switchover: creating syncer DCS switchover primary=${current_name} candidate=${candidate_name} "
23092311 if ! syncerctl_switchover " ${current_name} " " ${candidate_name} " " ${dcs_budget} " ; then
2312+ if [ " ${SWITCHOVER_SYNCERCTL_OUTCOME:- } " = " retryable_conflict" ]; then
2313+ # The syncer active-marker precheck promises mutation=0. Do not run the
2314+ # uncertain-DCS final-state/fence path: that path is reserved for calls
2315+ # where the client cannot prove whether this request persisted remote
2316+ # side effects. A stable active-marker conflict proves this request did
2317+ # not mutate DCS and must leave the already-running operation untouched.
2318+ return 1
2319+ fi
23102320 if switchover_final_state_already_reached " ${current_name} " " ${candidate_name} " " ${candidate_fqdn} " ; then
23112321 # The duplicate call proves that DCS/candidate convergence already
23122322 # reached the requested topology, but read_only=1 alone does not prove
@@ -2315,7 +2325,7 @@ run_switchover() {
23152325 idempotent_final_state_reached=" true"
23162326 else
23172327 # H2: do NOT roll the current primary back to writable here. syncerctl runs
2318- # with --force under a timeout(1) wrapper, so a non-zero/timeout return
2328+ # under a timeout(1) wrapper, so a non-zero/timeout return
23192329 # does not prove the DCS switchover record was not persisted — syncer may
23202330 # already be promoting the candidate. Unfencing the current primary would
23212331 # then make two writable primaries (split-brain). Fail closed by fencing
@@ -2382,6 +2392,7 @@ run_switchover() {
23822392 promoted_budget=$( stage_budget_or_exit " promote" " ${CANDIDATE_PROMOTED_VIA_SYNCERCTL_WAIT_SECONDS} " ) || return 1
23832393 log_switchover_info " Switchover stage candidate_promoted budget=${promoted_budget} s remaining_before=$( remaining_action_budget) s"
23842394 if ! wait_candidate_promoted_via_syncerctl " ${candidate_name} " " ${candidate_fqdn} " " ${promoted_budget} " ; then
2395+ log_switchover_error " Switchover failed: outcome=indeterminate_dcs_in_progress mutation=remote-partial stage=candidate_promoted reason=post_dcs_terminal_not_proven; old primary remains fenced and the DCS operation must not be preempted"
23852396 return 1
23862397 fi
23872398 # Post-stage overrun check (mirrors prepare/dcs/fence). The promote loop is
@@ -2401,6 +2412,7 @@ run_switchover() {
24012412 ready_budget=$( stage_budget_or_exit " ready" " ${CANDIDATE_REMOTE_ROOT_PRIMARY_READY_WAIT_SECONDS} " ) || return 1
24022413 log_switchover_info " Switchover stage candidate_primary_ready budget=${ready_budget} s remaining_before=$( remaining_action_budget) s"
24032414 if ! wait_candidate_remote_root_primary_ready " ${candidate_name} " " ${candidate_fqdn} " " ${ready_budget} " ; then
2415+ log_switchover_error " Switchover failed: outcome=indeterminate_dcs_in_progress mutation=remote-partial stage=candidate_primary_ready reason=post_dcs_terminal_not_proven; old primary remains fenced and the DCS operation must not be preempted"
24042416 return 1
24052417 fi
24062418 # Final-stage overrun check, BEFORE the success log/return. ready is the last
0 commit comments