Skip to content

Commit 56a05a3

Browse files
authored
Merge pull request #9728 from dolthub/elian/9725
#9725 - Fix AUTO_INCREMENT reuse after HA failover by refreshing trackers
2 parents e8faa08 + e1a9cb3 commit 56a05a3

File tree

2 files changed

+141
-0
lines changed

2 files changed

+141
-0
lines changed

go/libraries/doltcore/sqle/cluster/controller.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import (
5353
"github.com/dolthub/dolt/go/libraries/doltcore/sqle"
5454
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/clusterdb"
5555
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
56+
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/globalstate"
5657
"github.com/dolthub/dolt/go/libraries/utils/config"
5758
"github.com/dolthub/dolt/go/libraries/utils/filesys"
5859
"github.com/dolthub/dolt/go/libraries/utils/jwtauth"
@@ -930,6 +931,12 @@ func (c *Controller) immediateTransitionToStandby() error {
930931
func (c *Controller) transitionToPrimary(saveConnID int) error {
931932
c.setProviderIsStandby(false)
932933
c.killRunningQueries(saveConnID)
934+
// Promotion: ensure next AUTO_INCREMENT values do not reuse existing IDs.
935+
// Refresh trackers for databases already loaded in this session so first
936+
// writes on the new primary allocate IDs at or above current table maxima.
937+
if err := c.refreshAutoIncrementTrackersForSessionDatabases(); err != nil {
938+
return err
939+
}
933940
return nil
934941
}
935942

@@ -947,6 +954,69 @@ func (c *Controller) killRunningQueries(saveConnID int) {
947954
}
948955
}
949956

957+
// refreshAutoIncrementTrackersForSessionDatabases re-initializes AUTO_INCREMENT
958+
// trackers for databases already loaded in the current session by using their
959+
// working sets. This immediately aligns in-memory sequences with replicated
960+
// table metadata after a promotion so the first post-promotion inserts do not
961+
// reuse primary keys. This method only refreshes session-known databases; any
962+
// database not yet loaded is skipped here and its tracker initializes lazily on
963+
// first use. Only working sets are read; branches and remotes are not scanned
964+
// as part of this promotion path.
965+
func (c *Controller) refreshAutoIncrementTrackersForSessionDatabases() error {
966+
if c == nil || c.sqlCtxFactory == nil {
967+
return fmt.Errorf("cluster/controller: auto-inc refresh: missing sql context factory")
968+
}
969+
970+
// Create a SQL context to access session and provider
971+
sqlCtx, err := c.sqlCtxFactory(context.Background())
972+
if err != nil {
973+
return fmt.Errorf("cluster/controller: auto-inc refresh: create sql context: %w", err)
974+
}
975+
// Ensure proper session lifecycle for any storage-layer ops
976+
defer sql.SessionEnd(sqlCtx.Session)
977+
sql.SessionCommandBegin(sqlCtx.Session)
978+
defer sql.SessionCommandEnd(sqlCtx.Session)
979+
980+
sess := dsess.DSessFromSess(sqlCtx.Session)
981+
provider := sess.Provider()
982+
983+
for _, sdb := range provider.DoltDatabases() {
984+
name := sdb.Name()
985+
if name == clusterdb.DoltClusterDbName {
986+
continue
987+
}
988+
989+
// Load DB and its AI tracker
990+
db, err := provider.Database(sqlCtx, name)
991+
if err != nil {
992+
return fmt.Errorf("cluster/controller: auto-inc refresh: %s: provider db: %w", name, err)
993+
}
994+
gsp, ok := db.(globalstate.GlobalStateProvider)
995+
if !ok {
996+
// Non-versioned DBs don't participate in AUTO_INCREMENT global state
997+
continue
998+
}
999+
ai, err := gsp.GetGlobalState().AutoIncrementTracker(sqlCtx)
1000+
if err != nil {
1001+
return fmt.Errorf("cluster/controller: auto-inc refresh: %s: tracker: %w", name, err)
1002+
}
1003+
1004+
// Get working set roots only
1005+
state, ok, err := sess.LookupDbState(sqlCtx, name)
1006+
if err != nil {
1007+
return fmt.Errorf("cluster/controller: auto-inc refresh: %s: lookup db state: %w", name, err)
1008+
}
1009+
if !ok || state.WorkingSet() == nil {
1010+
// Not loaded in session; defer to lazy initialization on first use
1011+
continue
1012+
}
1013+
if err := ai.InitWithRoots(sqlCtx, state.WorkingSet()); err != nil {
1014+
return fmt.Errorf("cluster/controller: auto-inc refresh: %s: init: %w", name, err)
1015+
}
1016+
}
1017+
return nil
1018+
}
1019+
9501020
// called with c.mu held
9511021
func (c *Controller) setProviderIsStandby(standby bool) {
9521022
if c.standbyCallback != nil {

integration-tests/go-sql-server-driver/tests/sql-server-cluster.yaml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2028,3 +2028,74 @@ tests:
20282028
- on: server1
20292029
queries:
20302030
- exec: "set foreign_key_checks=0"
2031+
2032+
- name: auto_increment maintained across failover
2033+
multi_repos:
2034+
- name: server1
2035+
with_files:
2036+
- name: server.yaml
2037+
contents: |
2038+
log_level: trace
2039+
listener:
2040+
host: 0.0.0.0
2041+
port: {{get_port "server1"}}
2042+
cluster:
2043+
standby_remotes:
2044+
- name: standby
2045+
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
2046+
bootstrap_role: primary
2047+
bootstrap_epoch: 1
2048+
remotesapi:
2049+
port: {{get_port "server1_cluster"}}
2050+
server:
2051+
args: ["--config", "server.yaml"]
2052+
dynamic_port: server1
2053+
- name: server2
2054+
with_files:
2055+
- name: server.yaml
2056+
contents: |
2057+
log_level: trace
2058+
listener:
2059+
host: 0.0.0.0
2060+
port: {{get_port "server2"}}
2061+
cluster:
2062+
standby_remotes:
2063+
- name: standby
2064+
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
2065+
bootstrap_role: standby
2066+
bootstrap_epoch: 1
2067+
remotesapi:
2068+
port: {{get_port "server2_cluster"}}
2069+
server:
2070+
args: ["--config", "server.yaml"]
2071+
dynamic_port: server2
2072+
connections:
2073+
- on: server1
2074+
queries:
2075+
- exec: 'CREATE DATABASE repo1'
2076+
- exec: 'USE repo1'
2077+
- exec: 'SET @@GLOBAL.dolt_cluster_ack_writes_timeout_secs = 10'
2078+
- exec: 'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, v INT)'
2079+
- exec: 'INSERT INTO t(v) VALUES (0),(0),(0),(0),(0)'
2080+
- on: server2
2081+
queries:
2082+
- exec: 'USE repo1'
2083+
- query: 'SELECT * FROM t ORDER BY id'
2084+
result:
2085+
columns: ['id', 'v']
2086+
rows: [['1','0'], ['2','0'], ['3','0'], ['4','0'], ['5','0']]
2087+
retry_attempts: 100
2088+
- on: server1
2089+
queries:
2090+
- exec: "CALL dolt_assume_cluster_role('standby', 2)"
2091+
- on: server2
2092+
queries:
2093+
- exec: "CALL dolt_assume_cluster_role('primary', 2)"
2094+
- on: server2
2095+
queries:
2096+
- exec: 'USE repo1'
2097+
- exec: 'INSERT INTO t(v) VALUES (1)'
2098+
- query: 'SELECT * FROM t ORDER BY id'
2099+
result:
2100+
columns: ['id', 'v']
2101+
rows: [['1','0'], ['2','0'], ['3','0'], ['4','0'], ['5','0'], ['6','1']]

0 commit comments

Comments
 (0)