Skip to content

Commit 13d703f

Browse files
committed
Allow zodan add_node to work with lolor large object data.
Permit the new node to have lolor installed as long as its tables are empty, and require it when the source replicates lolor tables. Keep lolor out of create_sub's auto-detected skip_schema so the initial data sync copies the large object data. Add a TAP test covering the rejection cases and end-to-end sync.
1 parent 5ed173c commit 13d703f

5 files changed

Lines changed: 242 additions & 42 deletions

File tree

samples/Z0DAN/zodan.sql

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -284,10 +284,13 @@ BEGIN
284284

285285
IF synchronize_structure THEN
286286
BEGIN
287-
-- Query existing schemas on the new node (excluding system schemas)
287+
-- Query existing schemas on the new node (excluding system schemas).
288+
-- lolor is excluded here: Spock already keeps it out of the
289+
-- structure dump globally, and putting it into skip_schema would
290+
-- also exclude the lolor tables from the initial data sync.
288291
remotesql_schema := 'SELECT string_agg(schema_name, '','') as schemas
289292
FROM information_schema.schemata
290-
WHERE schema_name NOT IN (''information_schema'', ''pg_catalog'', ''pg_toast'', ''spock'', ''public'')
293+
WHERE schema_name NOT IN (''information_schema'', ''pg_catalog'', ''pg_toast'', ''spock'', ''public'', ''lolor'')
291294
AND schema_name NOT LIKE ''pg_temp_%''
292295
AND schema_name NOT LIKE ''pg_toast_temp_%''';
293296

@@ -1143,20 +1146,41 @@ BEGIN
11431146
RAISE EXCEPTION 'Exiting add_node: Database % does not exist on new node. Please create it first.', new_db_name;
11441147
END;
11451148

1146-
-- Check if they previously installed lolor on the destination.
1147-
-- They should not have run CREATE EXTENSION yet
1149+
-- Check lolor state on the destination. Having the lolor extension
1150+
-- installed is fine (and required when the source replicates lolor
1151+
-- tables), but pre-existing large object data would conflict with the
1152+
-- data synchronized from the source.
11481153
DECLARE
1149-
user_table_count integer;
1154+
new_lolor_installed boolean;
1155+
lolor_row_count bigint;
1156+
src_lolor_repset_tables integer;
11501157
remotesql text;
11511158
BEGIN
1152-
remotesql := 'SELECT count(*) FROM pg_tables WHERE schemaname = ''lolor''';
1153-
SELECT * FROM dblink(new_node_dsn, remotesql) AS t(count integer) INTO user_table_count;
1159+
remotesql := 'SELECT EXISTS(SELECT 1 FROM pg_catalog.pg_extension WHERE extname = ''lolor'')';
1160+
SELECT * FROM dblink(new_node_dsn, remotesql) AS t(installed boolean) INTO new_lolor_installed;
11541161

1155-
IF user_table_count > 0 THEN
1156-
RAISE NOTICE ' [FAILED] %', rpad('Database ' || new_db_name || ' has the lolor extension installed or remaining lolor data.', 120, ' ');
1157-
RAISE EXCEPTION 'Exiting add_node: Database % has the lolor extension installed or remaining lolor user data.', new_db_name;
1162+
IF new_lolor_installed THEN
1163+
remotesql := 'SELECT (SELECT count(*) FROM lolor.pg_largeobject) + (SELECT count(*) FROM lolor.pg_largeobject_metadata)';
1164+
SELECT * FROM dblink(new_node_dsn, remotesql) AS t(row_count bigint) INTO lolor_row_count;
1165+
1166+
IF lolor_row_count > 0 THEN
1167+
RAISE NOTICE ' [FAILED] %', rpad('Database ' || new_db_name || ' has pre-existing large object data in the lolor tables', 120, ' ');
1168+
RAISE EXCEPTION 'Exiting add_node: Database % on new node has pre-existing large object data in the lolor tables. The lolor tables must be empty so the large object data from the source node can be synchronized.', new_db_name;
1169+
ELSE
1170+
RAISE NOTICE ' OK: %', rpad('Checking database ' || new_db_name || ' lolor tables are empty', 120, ' ');
1171+
END IF;
1172+
END IF;
1173+
1174+
-- If the source node replicates lolor tables, the new node must have
1175+
-- lolor installed or the initial data synchronization will fail.
1176+
remotesql := 'SELECT count(*) FROM spock.tables WHERE nspname = ''lolor'' AND set_name IS NOT NULL';
1177+
SELECT * FROM dblink(src_dsn, remotesql) AS t(count integer) INTO src_lolor_repset_tables;
1178+
1179+
IF src_lolor_repset_tables > 0 AND NOT new_lolor_installed THEN
1180+
RAISE NOTICE ' [FAILED] %', rpad('Source node replicates lolor tables but database ' || new_db_name || ' does not have lolor installed', 120, ' ');
1181+
RAISE EXCEPTION 'Exiting add_node: Source node replicates lolor tables but database % on new node does not have the lolor extension installed. Please run CREATE EXTENSION lolor on the new node first.', new_db_name;
11581182
ELSE
1159-
RAISE NOTICE ' OK: %', rpad('Checking database ' || new_db_name || ' to ensure lolor is not installed', 120, ' ');
1183+
RAISE NOTICE ' OK: %', rpad('Checking lolor requirements for database ' || new_db_name, 120, ' ');
11601184
END IF;
11611185
END;
11621186

@@ -1165,7 +1189,9 @@ BEGIN
11651189
user_table_count integer;
11661190
remotesql text;
11671191
BEGIN
1168-
remotesql := 'SELECT count(*) FROM pg_tables WHERE schemaname NOT IN (''information_schema'', ''pg_catalog'', ''pg_toast'', ''spock'') AND schemaname NOT LIKE ''pg_temp_%'' AND schemaname NOT LIKE ''pg_toast_temp_%''';
1192+
-- lolor tables are excluded: they belong to the lolor extension and
1193+
-- their emptiness is enforced by the lolor check above.
1194+
remotesql := 'SELECT count(*) FROM pg_tables WHERE schemaname NOT IN (''information_schema'', ''pg_catalog'', ''pg_toast'', ''spock'', ''lolor'') AND schemaname NOT LIKE ''pg_temp_%'' AND schemaname NOT LIKE ''pg_toast_temp_%''';
11691195
SELECT * FROM dblink(new_node_dsn, remotesql) AS t(count integer) INTO user_table_count;
11701196

11711197
IF user_table_count > 0 THEN
@@ -2539,6 +2565,9 @@ DECLARE
25392565
new_version text;
25402566
sub_count text;
25412567
user_table_count text;
2568+
new_lolor_installed boolean;
2569+
lolor_row_count bigint;
2570+
src_lolor_repset_tables integer;
25422571
BEGIN
25432572
RAISE NOTICE '';
25442573
RAISE NOTICE '================================================================================';
@@ -2668,28 +2697,43 @@ BEGIN
26682697
-- Check 8: Database prerequisites (pre-check only)
26692698
-- ========================================================================
26702699
IF check_type = 'pre' AND new_node_dsn IS NOT NULL THEN
2671-
-- Check 8a: Verify lolor extension is not installed
2700+
-- Check 8a: lolor state. Having lolor installed is OK, but the lolor
2701+
-- tables must be empty, and if the source replicates lolor tables the
2702+
-- new node must have lolor installed.
26722703
BEGIN
2673-
remotesql := 'SELECT count(*) FROM pg_tables WHERE schemaname = ''lolor''';
2674-
SELECT * FROM dblink(new_node_dsn, remotesql) AS t(table_count text) INTO user_table_count;
2704+
remotesql := 'SELECT EXISTS(SELECT 1 FROM pg_catalog.pg_extension WHERE extname = ''lolor'')';
2705+
SELECT * FROM dblink(new_node_dsn, remotesql) AS t(installed boolean) INTO new_lolor_installed;
26752706

2676-
IF user_table_count IS NOT NULL AND user_table_count::integer = 0 THEN
2677-
RAISE NOTICE 'PASS: Destination database does not have signs of lolor being installed';
2678-
checks_passed := checks_passed + 1;
2707+
IF new_lolor_installed THEN
2708+
remotesql := 'SELECT (SELECT count(*) FROM lolor.pg_largeobject) + (SELECT count(*) FROM lolor.pg_largeobject_metadata)';
2709+
SELECT * FROM dblink(new_node_dsn, remotesql) AS t(row_count bigint) INTO lolor_row_count;
26792710
ELSE
2680-
RAISE NOTICE 'FAIL: Destination database has the lolor extension installed or remaining lolor user data in the lolor schema';
2711+
lolor_row_count := 0;
2712+
END IF;
2713+
2714+
remotesql := 'SELECT count(*) FROM spock.tables WHERE nspname = ''lolor'' AND set_name IS NOT NULL';
2715+
SELECT * FROM dblink(src_dsn, remotesql) AS t(count integer) INTO src_lolor_repset_tables;
2716+
2717+
IF lolor_row_count > 0 THEN
2718+
RAISE NOTICE 'FAIL: Destination database has pre-existing large object data in the lolor tables';
2719+
checks_failed := checks_failed + 1;
2720+
ELSIF src_lolor_repset_tables > 0 AND NOT new_lolor_installed THEN
2721+
RAISE NOTICE 'FAIL: Source node replicates lolor tables but the destination database does not have the lolor extension installed';
26812722
checks_failed := checks_failed + 1;
2723+
ELSE
2724+
RAISE NOTICE 'PASS: Destination database lolor state is compatible with add_node';
2725+
checks_passed := checks_passed + 1;
26822726
END IF;
26832727
EXCEPTION WHEN OTHERS THEN
2684-
RAISE NOTICE 'FAIL: lolor extension check - %', SQLERRM;
2728+
RAISE NOTICE 'FAIL: lolor check - %', SQLERRM;
26852729
checks_failed := checks_failed + 1;
26862730
END;
26872731

26882732
-- Check 8b: Verify database is empty (no user tables)
26892733
BEGIN
26902734
remotesql := $pg_tables$
26912735
SELECT count(*) FROM pg_tables
2692-
WHERE schemaname NOT IN ('information_schema', 'pg_catalog', 'pg_toast', 'spock')
2736+
WHERE schemaname NOT IN ('information_schema', 'pg_catalog', 'pg_toast', 'spock', 'lolor')
26932737
AND schemaname NOT LIKE 'pg_temp_%'
26942738
AND schemaname NOT LIKE 'pg_toast_temp_%'
26952739
$pg_tables$;

tests/tap/schedule

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ test: 024_node_id_collision
5050
test: 025_tiebreaker_equal_warning
5151
test: 030_autoddl_repset_stickiness
5252
test: 032_lolor_largeobject_repset
53+
test: 033_zodan_lolor_add_node
5354
test: 044_apply_change_logging
5455
# Upgrade schema match test (builds from source, slow):
5556
#test: 018_upgrade_schema_match

tests/tap/t/032_lolor_largeobject_repset.pl

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use Test::More;
44
use lib '.';
55
use SpockTest qw(create_cluster destroy_cluster system_maybe get_test_config
6-
scalar_query wait_for_sub_status);
6+
scalar_query wait_for_sub_status ensure_lolor);
77

88
# lolor stores large objects in ordinary tables in the "lolor" schema. Those
99
# tables must be allowed into a replication set, otherwise a DROP EXTENSION
@@ -12,14 +12,11 @@
1212
# that other protected schemas (spock) stay blocked, and that add-node
1313
# structure sync still works when lolor is present on both ends.
1414

15-
my $LOLOR_URL = 'https://github.com/pgEdge/lolor.git';
16-
1715
my $cfg = get_test_config();
1816
my $PG = $cfg->{pg_bin};
1917
my $DB = $cfg->{db_name};
2018
my $USER = $cfg->{db_user};
2119
my $HOST = $cfg->{host};
22-
my $LOGD = $cfg->{log_dir};
2320

2421
# psql helper: 1-based node index, returns true on success (output goes to log).
2522
sub psql_ok {
@@ -29,23 +26,6 @@ sub psql_ok {
2926
'-v', 'ON_ERROR_STOP=1', '-c', $sql);
3027
}
3128

32-
# Build and install lolor if it is not already present. Skip the whole test
33-
# (rather than fail) when it cannot be fetched or built, e.g. no network.
34-
sub ensure_lolor {
35-
my $sharedir = `$PG/pg_config --sharedir`;
36-
chomp $sharedir;
37-
return 1 if $sharedir && -f "$sharedir/extension/lolor.control";
38-
39-
my $build = "/tmp/spock_lolor_build";
40-
my $log = "$LOGD/032_lolor_build.log";
41-
system('rm', '-rf', $build);
42-
my $rc = system("git clone --depth 1 $LOLOR_URL $build >> '$log' 2>&1");
43-
return 0 if $rc != 0;
44-
$rc = system("make -C $build USE_PGXS=1 PG_CONFIG='$PG/pg_config' install >> '$log' 2>&1");
45-
return 0 if $rc != 0;
46-
return ($sharedir && -f "$sharedir/extension/lolor.control") ? 1 : 0;
47-
}
48-
4929
plan skip_all => "lolor extension unavailable (clone/build failed)"
5030
unless ensure_lolor();
5131

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
use strict;
2+
use warnings;
3+
use Test::More;
4+
use lib '.';
5+
use lib 't';
6+
use SpockTest qw(create_cluster destroy_cluster system_or_bail system_maybe
7+
get_test_config cross_wire scalar_query ensure_lolor);
8+
9+
# Zodan add_node with lolor large objects. The source cluster replicates the
10+
# lolor tables (lolor.pg_largeobject, lolor.pg_largeobject_metadata) in the
11+
# default replication set. Adding a node through zodan's add_node() must:
12+
# - reject a new node that lacks the lolor extension (data sync would fail),
13+
# - reject a new node whose lolor tables already contain data,
14+
# - accept a new node with lolor installed and empty, and copy the large
15+
# object data from the source during the initial data sync,
16+
# - stream large objects created after the join.
17+
18+
my $cfg = get_test_config();
19+
my $PG = $cfg->{pg_bin};
20+
my $DB = $cfg->{db_name};
21+
my $USER = $cfg->{db_user};
22+
my $PASS = $cfg->{db_password};
23+
my $HOST = $cfg->{host};
24+
25+
plan skip_all => "lolor extension unavailable (clone/build failed)"
26+
unless ensure_lolor();
27+
28+
# psql helper: 1-based node index, returns true on success (output to log).
29+
sub psql_ok {
30+
my ($node, $sql) = @_;
31+
my $port = $cfg->{node_ports}[$node - 1];
32+
return system_maybe("$PG/psql", '-X', '-p', $port, '-d', $DB,
33+
'-v', 'ON_ERROR_STOP=1', '-c', $sql);
34+
}
35+
36+
# Run SQL and capture combined stdout/stderr without going through a shell,
37+
# so quoting inside the SQL is preserved. Returns (exit_code, output).
38+
sub psql_capture {
39+
my ($node, $sql) = @_;
40+
my $port = $cfg->{node_ports}[$node - 1];
41+
my $pid = open(my $fh, '-|');
42+
die "fork failed: $!" unless defined $pid;
43+
if ($pid == 0) {
44+
open(STDERR, '>&', \*STDOUT) or die "cannot dup STDERR: $!";
45+
exec("$PG/psql", '-X', '-p', $port, '-d', $DB,
46+
'-v', 'ON_ERROR_STOP=1', '-c', $sql);
47+
exit 127;
48+
}
49+
my $out = do { local $/; <$fh> } // '';
50+
close($fh);
51+
return ($? >> 8, $out);
52+
}
53+
54+
# Poll a scalar query on a node until it returns $expected.
55+
sub wait_for_scalar {
56+
my ($node, $sql, $expected, $timeout) = @_;
57+
$timeout //= 60;
58+
for (1 .. $timeout) {
59+
my $v = scalar_query($node, $sql);
60+
return 1 if defined $v && $v eq $expected;
61+
sleep(1);
62+
}
63+
return 0;
64+
}
65+
66+
sub dsn {
67+
my ($node) = @_;
68+
my $port = $cfg->{node_ports}[$node - 1];
69+
return "host=$HOST dbname=$DB port=$port user=$USER password=$PASS";
70+
}
71+
72+
# --- Cluster setup: n1/n2 cross-wired, n3 is a blank target for zodan -------
73+
74+
create_cluster(3, 'Create 3 instances for zodan lolor test');
75+
cross_wire(2, ['n1', 'n2'], 'Cross-wire nodes n1 and n2');
76+
77+
# create_cluster registered a spock node on n3; drop it so n3 looks like a
78+
# freshly prepared instance (spock + dblink installed, no node/repsets).
79+
ok(psql_ok(3, "SELECT spock.node_drop('n3')"), 'n3 spock node registration dropped');
80+
ok(psql_ok(3, "CREATE EXTENSION IF NOT EXISTS dblink"), 'dblink installed on n3');
81+
82+
# lolor on the source cluster, its tables in the default replication set.
83+
# n1 and n2 are cross-wired with automatic DDL replication, so CREATE
84+
# EXTENSION on n1 arrives on n2 by itself.
85+
ok(psql_ok(1, "CREATE EXTENSION lolor"), 'lolor installed on n1');
86+
ok(wait_for_scalar(2, "SELECT count(*) FROM pg_extension WHERE extname = 'lolor'", '1'),
87+
'lolor arrived on n2 via DDL replication');
88+
for my $node (1, 2) {
89+
ok(psql_ok($node, "SELECT spock.repset_add_table('default', 'lolor.pg_largeobject')"),
90+
"lolor.pg_largeobject in default repset on n$node");
91+
ok(psql_ok($node, "SELECT spock.repset_add_table('default', 'lolor.pg_largeobject_metadata')"),
92+
"lolor.pg_largeobject_metadata in default repset on n$node");
93+
}
94+
95+
# Large object on n1; sanity-check it reaches n2 before involving zodan.
96+
ok(psql_ok(1, "SET lolor.node=1; SELECT lo_from_bytea(0, '\\xdeadbeefcafe')"),
97+
'large object created on n1');
98+
ok(wait_for_scalar(2, "SELECT count(*) FROM lolor.pg_largeobject WHERE encode(data, 'hex') = 'deadbeefcafe'", '1'),
99+
'large object replicated from n1 to n2');
100+
101+
# Load the zodan procedures on the node being added.
102+
system_or_bail("$PG/psql", '-X', '-p', $cfg->{node_ports}[2], '-d', $DB,
103+
'-v', 'ON_ERROR_STOP=1', '-f', '../../samples/Z0DAN/zodan.sql');
104+
pass('zodan procedures loaded on n3');
105+
106+
my $add_node_sql =
107+
"CALL spock.add_node('n1', '" . dsn(1) . "', 'n3', '" . dsn(3) . "', " .
108+
"true, 'CA', 'USA', '{}'::jsonb)";
109+
110+
# --- Negative: source replicates lolor but n3 has no lolor extension --------
111+
112+
my ($rc, $out) = psql_capture(3, $add_node_sql);
113+
ok($rc != 0, 'add_node rejected while n3 lacks the lolor extension');
114+
like($out, qr/does not have the lolor extension installed/,
115+
'rejection message asks for CREATE EXTENSION lolor');
116+
117+
# --- Negative: n3 has lolor installed but with pre-existing data ------------
118+
119+
ok(psql_ok(3, "CREATE EXTENSION lolor"), 'lolor installed on n3');
120+
ok(psql_ok(3, "SET lolor.node=3; SELECT lo_from_bytea(0, '\\x0bad0bad')"),
121+
'pre-existing large object created on n3');
122+
123+
($rc, $out) = psql_capture(3, $add_node_sql);
124+
ok($rc != 0, 'add_node rejected while n3 has pre-existing lolor data');
125+
like($out, qr/pre-existing large object data/,
126+
'rejection message mentions pre-existing large object data');
127+
128+
# health_check 'pre' must report the same problem without raising.
129+
($rc, $out) = psql_capture(3,
130+
"CALL spock.health_check('n1', '" . dsn(1) . "', 'n3', '" . dsn(3) . "', 'pre', false)");
131+
like($out, qr/FAIL: Destination database has pre-existing large object data/,
132+
'health_check pre-check flags pre-existing lolor data');
133+
134+
# --- Positive: empty lolor tables, add_node copies the data -----------------
135+
136+
ok(psql_ok(3, "DELETE FROM lolor.pg_largeobject; DELETE FROM lolor.pg_largeobject_metadata"),
137+
'pre-existing lolor data cleared on n3');
138+
139+
($rc, $out) = psql_capture(3, $add_node_sql);
140+
is($rc, 0, 'add_node succeeded with lolor installed and empty') or diag($out);
141+
142+
ok(wait_for_scalar(3, "SELECT count(*) FROM lolor.pg_largeobject WHERE encode(data, 'hex') = 'deadbeefcafe'", '1'),
143+
'existing large object data copied to n3 by initial sync');
144+
ok(wait_for_scalar(3, "SELECT count(*) FROM lolor.pg_largeobject_metadata", '1'),
145+
'large object metadata copied to n3');
146+
147+
# --- Streaming: a large object created after the join reaches n3 ------------
148+
149+
ok(psql_ok(1, "SET lolor.node=1; SELECT lo_from_bytea(0, '\\xfeedface')"),
150+
'second large object created on n1 after join');
151+
ok(wait_for_scalar(3, "SELECT count(*) FROM lolor.pg_largeobject WHERE encode(data, 'hex') = 'feedface'", '1'),
152+
'post-join large object streamed to n3');
153+
154+
destroy_cluster('Destroy zodan lolor test cluster');
155+
156+
done_testing();

tests/tap/t/SpockTest.pm

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ our @EXPORT_OK = qw(
2323
wait_for_sub_status
2424
wait_for_exception_log
2525
wait_for_pg_ready
26+
ensure_lolor
2627
);
2728

2829
# Test configuration
@@ -149,6 +150,24 @@ sub system_maybe {
149150
return $rc == 0;
150151
}
151152

153+
# Build and install the lolor extension if it is not already present.
154+
# Returns true when lolor is available. Callers should skip_all (rather
155+
# than fail) when this returns false, e.g. no network to clone the repo.
156+
sub ensure_lolor {
157+
my $sharedir = `$PG_BIN/pg_config --sharedir`;
158+
chomp $sharedir;
159+
return 1 if $sharedir && -f "$sharedir/extension/lolor.control";
160+
161+
my $build = "/tmp/spock_lolor_build";
162+
my $log = "$LOG_DIR/lolor_build.log";
163+
system('rm', '-rf', $build);
164+
my $rc = system("git clone --depth 1 https://github.com/pgEdge/lolor.git $build >> '$log' 2>&1");
165+
return 0 if $rc != 0;
166+
$rc = system("make -C $build USE_PGXS=1 PG_CONFIG='$PG_BIN/pg_config' install >> '$log' 2>&1");
167+
return 0 if $rc != 0;
168+
return ($sharedir && -f "$sharedir/extension/lolor.control") ? 1 : 0;
169+
}
170+
152171
# Create PostgreSQL configuration file
153172
sub create_postgresql_conf {
154173
my ($datadir, $port) = @_;

0 commit comments

Comments
 (0)