Skip to content

Commit eae033e

Browse files
authored
Merge pull request #132 from ProxySQL/fix/131-replica-ready-after-deploy
fix(replication): wait for replicas to be ready to serve queries after deploy (fixes #131)
2 parents c097065 + 070adf4 commit eae033e

5 files changed

Lines changed: 142 additions & 0 deletions

File tree

sandbox/sandbox_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,39 @@ func testCreateReplicationSandbox(t *testing.T) {
626626
t.Fatalf(globals.ErrCreatingSandbox, err)
627627
}
628628

629+
// #131 fix test: replicas must answer queries immediately (exercises wait in initialize_slaves*)
629630
sandboxDir := path.Join(sandboxDef.SandboxDir, defaults.Defaults().MasterSlavePrefix+pathVersion)
631+
for _, n := range []string{"1", "2"} {
632+
use := path.Join(sandboxDir, defaults.Defaults().NodePrefix+n, "use")
633+
if !common.ExecExists(use) {
634+
t.Fatalf("expected replica use script at %s", use)
635+
}
636+
// RunCmdWithArgs (RunCmd takes only the cmd string)
637+
out, err := common.RunCmdWithArgs(use, []string{"-BN", "-e", "SELECT 1;"}); // tests #131 fix: replica must be ready immediately (no sleep) after deploy
638+
if err != nil {
639+
t.Fatalf("replica n%s not ready immediately after deploy replication: %v\noutput: %s", n, err, out)
640+
}
641+
if strings.TrimSpace(out) != "1" {
642+
t.Fatalf("replica n%s: expected '1', got %q", n, out)
643+
}
644+
}
645+
t.Logf("ok - both replicas accepted queries immediately after deploy (no manual sleep)")
646+
647+
// Directly assert the generated script contains the waits (the actual fix).
648+
initSlavesScript := path.Join(sandboxDir, "initialize_slaves")
649+
initContent, rerr := os.ReadFile(initSlavesScript)
650+
if rerr != nil {
651+
t.Fatalf("could not read initialize_slaves: %v", rerr)
652+
}
653+
initStr := string(initContent)
654+
if !strings.Contains(initStr, `wait_until_replica_ready "$SBDIR/n1/use" 60 1`) {
655+
t.Errorf("initialize_slaves missing wait_until_replica_ready for n1 (the #131 fix)")
656+
}
657+
if !strings.Contains(initStr, `wait_until_replica_ready "$SBDIR/n2/use" 60 1`) {
658+
t.Errorf("initialize_slaves missing wait_until_replica_ready for n2 (the #131 fix)")
659+
}
660+
661+
sandboxDir = path.Join(sandboxDef.SandboxDir, defaults.Defaults().MasterSlavePrefix+pathVersion)
630662
okDirExists(t, sandboxDir)
631663
dirs := []string{
632664
defaults.Defaults().MasterName,

sandbox/templates/replication/init_slaves.gotxt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@
66
# This script is called by 'start_all' when needed
77
SBDIR={{.SandboxDir}}
88
cd "$SBDIR"
9+
10+
# Wait until a replica is ready to accept queries from the sandbox user.
11+
# This is called after START REPLICA (or START SLAVE) during replication
12+
# initialization to prevent the race where the first client command on a
13+
# just-started replica fails with "Access denied" or connection errors.
14+
# Observed especially with MySQL 8.4+ and 9.x.
15+
wait_until_replica_ready() {
16+
local use_cmd=${1:-$SBDIR/use}
17+
local max_attempts=${2:-60}
18+
local sleep_sec=${3:-1}
19+
for i in $(seq 1 $max_attempts); do
20+
if $use_cmd -BN -e "SELECT 1;" >/dev/null 2>&1 ; then
21+
return 0
22+
fi
23+
sleep $sleep_sec
24+
done
25+
echo "WARNING: replica not ready to accept queries after $((max_attempts * sleep_sec))s (cmd: $use_cmd)" >&2
26+
return 1
27+
}
28+
929
# workaround for Bug#89959
1030
$SBDIR/{{.MasterLabel}}/use -h {{.MasterIp}} -u {{.RplUser}} -p{{.RplPassword}} -e 'set @a=1'
1131
if [ ! -f needs_initialization ]
@@ -18,6 +38,8 @@ fi
1838
echo "initializing {{.SlaveLabel}} {{.Node}}"
1939
echo '{{.ChangeMasterTo}} {{.MasterHostParam}}="{{.MasterIp}}", {{.MasterPortParam}}={{.MasterPort}}, {{.MasterUserParam}}="{{.RplUser}}", {{.MasterPasswordParam}}="{{.RplPassword}}" {{.MasterAutoPosition}} {{.ChangeMasterExtra}}' | $SBDIR/{{.NodeLabel}}{{.Node}}/use -u root
2040
$SBDIR/{{.NodeLabel}}{{.Node}}/use -u root -e '{{.StartReplica}}'
41+
# Ensure the replica is serving queries before we return from initialization.
42+
wait_until_replica_ready "$SBDIR/{{.NodeLabel}}{{.Node}}/use" 60 1
2143
{{end}}
2244
if [ -x ./post_initialization ]
2345
then

sandbox/templates/replication/init_slaves_84.gotxt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@
77
# Uses MySQL 8.4+ replication syntax (CHANGE REPLICATION SOURCE TO)
88
SBDIR={{.SandboxDir}}
99
cd "$SBDIR"
10+
11+
# Wait until a replica is ready to accept queries from the sandbox user.
12+
# This is called after START REPLICA (or START SLAVE) during replication
13+
# initialization to prevent the race where the first client command on a
14+
# just-started replica fails with "Access denied" or connection errors.
15+
# Observed especially with MySQL 8.4+ and 9.x.
16+
wait_until_replica_ready() {
17+
local use_cmd=${1:-$SBDIR/use}
18+
local max_attempts=${2:-60}
19+
local sleep_sec=${3:-1}
20+
for i in $(seq 1 $max_attempts); do
21+
if $use_cmd -BN -e "SELECT 1;" >/dev/null 2>&1 ; then
22+
return 0
23+
fi
24+
sleep $sleep_sec
25+
done
26+
echo "WARNING: replica not ready to accept queries after $((max_attempts * sleep_sec))s (cmd: $use_cmd)" >&2
27+
return 1
28+
}
29+
1030
# workaround for Bug#89959
1131
$SBDIR/{{.MasterLabel}}/use -h {{.MasterIp}} -u {{.RplUser}} -p{{.RplPassword}} -e 'set @a=1'
1232
if [ ! -f needs_initialization ]
@@ -19,6 +39,8 @@ fi
1939
echo "initializing {{.SlaveLabel}} {{.Node}}"
2040
echo '{{.ChangeMasterTo}} {{.MasterHostParam}}="{{.MasterIp}}", {{.MasterPortParam}}={{.MasterPort}}, {{.MasterUserParam}}="{{.RplUser}}", {{.MasterPasswordParam}}="{{.RplPassword}}" {{.MasterAutoPosition}} {{.ChangeMasterExtra}}' | $SBDIR/{{.NodeLabel}}{{.Node}}/use -u root
2141
$SBDIR/{{.NodeLabel}}{{.Node}}/use -u root -e '{{.StartReplica}}'
42+
# Ensure the replica is serving queries before we return from initialization.
43+
wait_until_replica_ready "$SBDIR/{{.NodeLabel}}{{.Node}}/use" 60 1
2244
{{end}}
2345
if [ -x ./post_initialization ]
2446
then

sandbox/templates/single/sb_include.gotxt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,24 @@ function wait_until_wsrep_ready
8484
echo "WARNING: wsrep_ready not ON after $((max_attempts * sleep_sec))s" >&2
8585
return 1
8686
}
87+
88+
# Wait until a replica is ready to accept queries from the sandbox user.
89+
# This is called after START REPLICA (or START SLAVE) during replication
90+
# initialization to prevent the race where the first client command on a
91+
# just-started replica fails with "Access denied" or connection errors.
92+
# Observed especially with MySQL 8.4+ and 9.x.
93+
# Usage: wait_until_replica_ready "$SBDIR/s1/use" 60 1
94+
function wait_until_replica_ready
95+
{
96+
local use_cmd=${1:-$SBDIR/use}
97+
local max_attempts=${2:-60}
98+
local sleep_sec=${3:-1}
99+
for i in $(seq 1 $max_attempts); do
100+
if $use_cmd -BN -e "SELECT 1;" >/dev/null 2>&1 ; then
101+
return 0
102+
fi
103+
sleep $sleep_sec
104+
done
105+
echo "WARNING: replica not ready to accept queries after $((max_attempts * sleep_sec))s (cmd: $use_cmd)" >&2
106+
return 1
107+
}

sandbox/templates_flavor_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,48 @@ func TestReplicationStopAndUse_DelegateToSingleTemplates(t *testing.T) {
177177
}
178178
}
179179
}
180+
181+
// TestInitSlavesTemplates_IncludeReplicaReadyWait ensures that the
182+
// replication initialization templates (used by "deploy replication")
183+
// contain the wait_until_replica_ready logic introduced to fix #131.
184+
// This is a pure template test and does not require any MySQL binaries.
185+
func TestInitSlavesTemplates_IncludeReplicaReadyWait(t *testing.T) {
186+
for name, tmplContent := range map[string]string{
187+
"init_slaves": initSlavesTemplate,
188+
"init_slaves_84": initSlaves84Template,
189+
} {
190+
data := common.StringMap{
191+
"ShellPath": "/bin/bash",
192+
"Copyright": "# test",
193+
"AppVersion": "test",
194+
"DateTime": "now",
195+
"TemplateName": name,
196+
"SandboxDir": "/tmp/rsandbox_1234",
197+
"MasterLabel": "master",
198+
"MasterIp": "127.0.0.1",
199+
"RplUser": "rsandbox",
200+
"RplPassword": "rsandbox",
201+
"MasterAutoPosition": "",
202+
"ChangeMasterExtra": "",
203+
"StartReplica": "START REPLICA",
204+
"NodeLabel": "n",
205+
"SlaveLabel": "slave",
206+
"Slaves": []common.StringMap{
207+
{"NodeLabel": "n", "Node": 1, "SlaveLabel": "slave"},
208+
{"NodeLabel": "n", "Node": 2, "SlaveLabel": "slave"},
209+
},
210+
}
211+
result := renderTemplate(t, tmplContent, data)
212+
213+
if !strings.Contains(result, "wait_until_replica_ready") {
214+
t.Errorf("%s template must define the wait_until_replica_ready helper (regression for #131)", name)
215+
}
216+
// The call must appear for each slave after the START REPLICA line.
217+
if !strings.Contains(result, `wait_until_replica_ready "$SBDIR/n1/use" 60 1`) {
218+
t.Errorf("%s template must invoke wait for the first replica", name)
219+
}
220+
if !strings.Contains(result, `wait_until_replica_ready "$SBDIR/n2/use" 60 1`) {
221+
t.Errorf("%s template must invoke wait for the second replica", name)
222+
}
223+
}
224+
}

0 commit comments

Comments
 (0)