Skip to content

Commit ef4ee6c

Browse files
authored
Merge pull request #123 from ProxySQL/feat/postgresql-test-replication-script
feat(postgresql): add test_replication script for replication topology
2 parents e175007 + fb4ffbd commit ef4ee6c

3 files changed

Lines changed: 127 additions & 0 deletions

File tree

cmd/replication.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,17 @@ func deployReplicationNonMySQL(cmd *cobra.Command, args []string, providerName s
163163
fmt.Printf("WARNING: could not write check_recovery script: %s\n", err)
164164
}
165165

166+
testReplOpts := postgresql.ScriptOptions{
167+
SandboxDir: topologyDir,
168+
BinDir: binDir,
169+
LibDir: libDir,
170+
Port: primaryPort,
171+
}
172+
testReplScript := postgresql.GenerateTestReplicationScript(testReplOpts, len(replicaPorts))
173+
if err := os.WriteFile(path.Join(topologyDir, "test_replication"), []byte(testReplScript), 0755); err != nil { //nolint:gosec // scripts must be executable
174+
fmt.Printf("WARNING: could not write test_replication script: %s\n", err)
175+
}
176+
166177
// Handle --with-proxysql
167178
withProxySQL, _ := flags.GetBool("with-proxysql")
168179
if withProxySQL {

providers/postgresql/postgresql_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,34 @@ func TestGenerateCheckRecoveryScript(t *testing.T) {
127127
}
128128
}
129129

130+
func TestGenerateTestReplicationScript(t *testing.T) {
131+
script := GenerateTestReplicationScript(ScriptOptions{
132+
SandboxDir: "/home/test/sandboxes/postgresql_repl_16614",
133+
BinDir: "/opt/postgresql/16.13/bin",
134+
LibDir: "/opt/postgresql/16.13/lib",
135+
Port: 16614,
136+
}, 2)
137+
expectedSubstrings := []string{
138+
`SBDIR=/home/test/sandboxes/postgresql_repl_16614`,
139+
"./primary/use",
140+
"pg_current_wal_lsn()",
141+
"pg_last_wal_replay_lsn()",
142+
"pg_is_in_recovery()",
143+
"./replica1/use",
144+
"./replica2/use",
145+
"test_summary",
146+
}
147+
for _, want := range expectedSubstrings {
148+
if !strings.Contains(script, want) {
149+
t.Errorf("script missing %q", want)
150+
}
151+
}
152+
// numReplicas=2 should NOT mention replica3
153+
if strings.Contains(script, "./replica3/use") {
154+
t.Error("script unexpectedly references replica3")
155+
}
156+
}
157+
130158
func TestGenerateScripts(t *testing.T) {
131159
opts := ScriptOptions{
132160
SandboxDir: "/tmp/pg_sandbox",

providers/postgresql/scripts.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,91 @@ func GenerateCheckRecoveryScript(opts ScriptOptions, replicaPorts []int) string
6060
}
6161
return b.String()
6262
}
63+
64+
// GenerateTestReplicationScript emits a topology-level test_replication
65+
// script that verifies replication is actually working: it creates a
66+
// table on the primary, inserts rows, captures the primary's WAL LSN,
67+
// then queries each replica to confirm the LSN matches and the row
68+
// count is in sync. Modeled on the MySQL `test_replication` template
69+
// (sandbox/templates/replication/test_replication.gotxt) but adapted to
70+
// PostgreSQL primitives (pg_current_wal_lsn / pg_last_wal_replay_lsn).
71+
//
72+
// numReplicas is the count of replica sub-sandboxes laid down by
73+
// `deploy replication --provider=postgresql` (i.e. one less than --nodes).
74+
// The generated script invokes ./primary/use and ./replicaN/use, which
75+
// the PostgreSQL provider already creates as per-sandbox `use` scripts.
76+
func GenerateTestReplicationScript(opts ScriptOptions, numReplicas int) string {
77+
preamble := fmt.Sprintf(envPreamble, opts.LibDir)
78+
var b strings.Builder
79+
b.WriteString(preamble)
80+
b.WriteString(fmt.Sprintf(`SBDIR=%s
81+
cd "$SBDIR"
82+
83+
if [ ! -x ./primary/use ]; then
84+
echo "# No primary found at ./primary/use"
85+
exit 1
86+
fi
87+
PRIMARY=./primary/use
88+
89+
FAILED=0
90+
PASSED=0
91+
92+
function ok_equal {
93+
fact="$1"
94+
expected="$2"
95+
msg="$3"
96+
if [ "$fact" = "$expected" ]; then
97+
echo "ok - (expected: <$expected>) - $msg"
98+
PASSED=$((PASSED+1))
99+
else
100+
echo "not ok - (expected: <$expected> found: <$fact>) - $msg"
101+
FAILED=$((FAILED+1))
102+
fi
103+
}
104+
105+
function test_summary {
106+
TESTS=$((PASSED+FAILED))
107+
printf "# Tests : %%5d\n" $TESTS
108+
printf "# Passed: %%5d\n" $PASSED
109+
printf "# Failed: %%5d\n" $FAILED
110+
if [ "$FAILED" != "0" ]; then exit 1; fi
111+
exit 0
112+
}
113+
114+
YYYY_MM=$(date +%%Y-%%m)
115+
echo "# Creating and populating table on primary..."
116+
$PRIMARY -qc 'DROP TABLE IF EXISTS dbdeployer_test_replication'
117+
$PRIMARY -qc 'CREATE TABLE dbdeployer_test_replication (i INT NOT NULL PRIMARY KEY, msg VARCHAR(50), d DATE, t TIME, dt TIMESTAMP)'
118+
for N in $(seq -f '%%02.0f' 1 20); do
119+
$PRIMARY -qc "INSERT INTO dbdeployer_test_replication VALUES ($N, 'test sandbox $N', '${YYYY_MM}-$N', '11:23:$N', '${YYYY_MM}-01 12:34:$N')"
120+
done
121+
122+
PRIMARY_RECS=$($PRIMARY -Atqc 'SELECT count(*) FROM dbdeployer_test_replication')
123+
PRIMARY_LSN=$($PRIMARY -Atqc 'SELECT pg_current_wal_lsn()')
124+
echo "# Primary: rows=$PRIMARY_RECS LSN=$PRIMARY_LSN"
125+
126+
# Allow a brief window for streaming replication to catch up
127+
sleep 1
128+
129+
`, opts.SandboxDir))
130+
131+
for n := 1; n <= numReplicas; n++ {
132+
b.WriteString(fmt.Sprintf(`echo "# Testing replica %d"
133+
if [ -x ./replica%d/use ]; then
134+
REPLICA_LSN=$(./replica%d/use -Atqc 'SELECT pg_last_wal_replay_lsn()')
135+
REPLICA_RECS=$(./replica%d/use -Atqc 'SELECT count(*) FROM dbdeployer_test_replication')
136+
REPLICA_IS_REPLICA=$(./replica%d/use -Atqc 'SELECT pg_is_in_recovery()')
137+
ok_equal "$REPLICA_IS_REPLICA" "t" "replica %d is in recovery mode"
138+
ok_equal "$REPLICA_LSN" "$PRIMARY_LSN" "replica %d LSN matches primary"
139+
ok_equal "$REPLICA_RECS" "$PRIMARY_RECS" "replica %d row count matches primary"
140+
else
141+
echo "not ok - replica %d/use not found"
142+
FAILED=$((FAILED+1))
143+
fi
144+
145+
`, n, n, n, n, n, n, n, n, n))
146+
}
147+
148+
b.WriteString("test_summary\n")
149+
return b.String()
150+
}

0 commit comments

Comments
 (0)