Skip to content

Commit b192a96

Browse files
ihalatciCopilot
andauthored
[CI only] Bump PG minors to 15.19 / 16.15 / 17.11 (#8778)
Validation PR for the-process [#241](citusdata/the-process#241). Repoints CI at the freshly built dev images carrying the new PostgreSQL minors published 2026-08-14/15. | | before | after | |---|---|---| | PG15 | 15.18 | 15.19 | | PG16 | 16.14 | 16.15 | | PG17 | 17.10 | 17.11 | `image_suffix`: `-dev-34d3b6f` --- ## Fallout from the new minors Validation surfaced two independent PostgreSQL security changes in this minor set that break Citus. ### 1. psql `COPY FROM STDIN` (fixed here) When a `COPY` fails early, the new psql silently swallows every line up to and including the next `\.`. Tests that relied on the old behaviour were adjusted: a terminating `\.` was added where one was missing, stray statements after a swallowed block were removed, and expected output was trimmed for the lines that no longer execute. ### 2. `output_plugin_libraries` (fixed here) PostgreSQL 14.24, 15.19, 16.15, 17.11 and 18.6 add an `output_plugin_libraries` GUC. Only the libraries it lists may be used as logical decoding output plugins. It defaults to `pgoutput, test_decoding` and is `superuser`-settable, so it takes a reload rather than a restart. **Upgrade note for operators.** Citus uses an output plugin named `citus` for logical replication during non-blocking shard splits. On these minors the following fail until the plugin is allowed: * `citus_split_shard_by_split_points(..., 'force_logical')` and `'auto'` * `create_distributed_table_concurrently()` * `citus_isolate_tenant_to_new_shard(..., 'force_logical'` / `'auto')` Shard **moves** and the rebalancer are unaffected -- they use `pgoutput`. CDC through the `pgoutput` shim is unaffected; CDC through `wal2json` needs the same treatment, exactly as it does on vanilla PostgreSQL. Remediation, on **every** node: ```sql ALTER SYSTEM SET output_plugin_libraries = pgoutput, test_decoding, citus; SELECT pg_reload_conf(); ``` The GUC is `GUC_LIST_QUOTE`, so quoting the whole list (`'pgoutput, test_decoding, citus'`) stores it as a single name and does **not** work. Use the bare list above, or quote each element individually. This PR does not work around the restriction -- allowing a decoder is deliberately an operator action. Instead Citus now **fails fast and clearly**: a preflight check runs against the source node before any shards, publications or replication slots are created, and raises an error naming the plugin, the node, the current allowlist value, and a ready-to-paste `ALTER SYSTEM` hint. ### Testing The regression and CDC harnesses start their own clusters, so in CI *we* are the operator: `pg_regress_multi.pl` and `cdctestlib.pm` now write the allowlist themselves, guarded by a probe of `postgresql.conf.sample` so they stay compatible with older minors where the GUC does not exist. To keep the un-remediated path covered, a new `check-split-output-plugin-denied` job deliberately skips that override (via `CITUS_TEST_SKIP_OUTPUT_PLUGIN_ALLOWLIST=1`) and asserts that a non-blocking split fails with the new error and leaks no shards, publications or replication slots. --- **Draft:** these are `-dev-` images. Do not merge until the-process [#241](citusdata/the-process#241) is merged and this PR is repointed at the resulting release image tag. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7c6370b2-06fd-4491-bf92-ecb811d34518
1 parent 136c7eb commit b192a96

19 files changed

Lines changed: 228 additions & 46 deletions

.github/workflows/build_and_test.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ jobs:
3131
pgupgrade_image_name: "ghcr.io/citusdata/pgupgradetester"
3232
style_checker_image_name: "ghcr.io/citusdata/stylechecker"
3333
style_checker_tools_version: "0.8.18"
34-
sql_snapshot_pg_version: "17.10"
35-
image_suffix: "-v88596db"
36-
pg15_version: '{ "major": "15", "full": "15.18" }'
37-
pg16_version: '{ "major": "16", "full": "16.14" }'
38-
pg17_version: '{ "major": "17", "full": "17.10" }'
39-
upgrade_pg_versions: "15.18-16.14-17.10"
34+
sql_snapshot_pg_version: "17.11"
35+
image_suffix: "-v7a09c75"
36+
pg15_version: '{ "major": "15", "full": "15.19" }'
37+
pg16_version: '{ "major": "16", "full": "16.15" }'
38+
pg17_version: '{ "major": "17", "full": "17.11" }'
39+
upgrade_pg_versions: "15.19-16.15-17.11"
4040
steps:
4141
# Since GHA jobs need at least one step we use a noop step here.
4242
- name: Set up parameters
@@ -144,7 +144,7 @@ jobs:
144144
${{ needs.params.outputs.pg16_version }},
145145
${{ needs.params.outputs.pg17_version }}
146146
]
147-
make_targets: '["check-split", "check-multi", "check-multi-1", "check-multi-1-create-citus", "check-multi-mx", "check-vanilla", "check-isolation", "check-operations", "check-follower-cluster", "check-add-backup-node", "check-columnar", "check-columnar-isolation", "check-enterprise", "check-enterprise-isolation", "check-enterprise-isolation-logicalrep-1", "check-enterprise-isolation-logicalrep-2", "check-enterprise-isolation-logicalrep-3"]'
147+
make_targets: '["check-split", "check-split-output-plugin-denied", "check-multi", "check-multi-1", "check-multi-1-create-citus", "check-multi-mx", "check-vanilla", "check-isolation", "check-operations", "check-follower-cluster", "check-add-backup-node", "check-columnar", "check-columnar-isolation", "check-enterprise", "check-enterprise-isolation", "check-enterprise-isolation-logicalrep-1", "check-enterprise-isolation-logicalrep-2", "check-enterprise-isolation-logicalrep-3"]'
148148
image_suffix: ${{ needs.params.outputs.image_suffix }}
149149
image_name: ${{ needs.params.outputs.test_image_name }}
150150
secrets:

src/backend/distributed/operations/shard_split.c

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ typedef struct GroupedDummyShards
6969
List *shardIntervals;
7070
} GroupedDummyShards;
7171

72+
/* name of the logical decoding output plugin used by non-blocking splits */
73+
#define CITUS_SPLIT_DECODER_PLUGIN "citus"
74+
7275
/* Function declarations */
7376
static void ErrorIfCannotSplitShard(SplitOperation splitOperation,
7477
ShardInterval *sourceShard);
@@ -92,6 +95,8 @@ static void CreateAuxiliaryStructuresForShardGroup(List *shardGroupSplitInterval
9295
List *workersForPlacementList,
9396
bool includeReplicaIdentity);
9497
static void CreateReplicaIdentitiesForDummyShards(HTAB *mapOfPlacementToDummyShardList);
98+
static void ErrorIfOutputPluginNotAllowed(MultiConnection *connection,
99+
char *outputPlugin);
95100
static void CreateObjectOnPlacement(List *objectCreationCommandList,
96101
WorkerNode *workerNode);
97102
static List * CreateSplitIntervalsForShardGroup(List *sourceColocatedShardList,
@@ -1365,6 +1370,73 @@ AcquireNonblockingSplitLock(Oid relationId)
13651370
}
13661371

13671372

1373+
/*
1374+
* ErrorIfOutputPluginNotAllowed errors out if the given logical decoding output
1375+
* plugin is not listed in the "output_plugin_libraries" setting on the node
1376+
* behind the given connection, which is where the replication slot is created.
1377+
*
1378+
* "output_plugin_libraries" was introduced in PostgreSQL 14.24, 15.19, 16.15,
1379+
* 17.11 and 18.6. On older minor versions current_setting() returns NULL and we
1380+
* skip the check, because there is nothing to enforce.
1381+
*/
1382+
static void
1383+
ErrorIfOutputPluginNotAllowed(MultiConnection *connection, char *outputPlugin)
1384+
{
1385+
StringInfo command = makeStringInfo();
1386+
appendStringInfo(command,
1387+
"SELECT current_setting('output_plugin_libraries', true), "
1388+
"coalesce(bool_or(btrim(name, ' \"') = %s), false) "
1389+
"FROM unnest(string_to_array(coalesce("
1390+
"current_setting('output_plugin_libraries', true), %s), ',')) name",
1391+
quote_literal_cstr(outputPlugin),
1392+
quote_literal_cstr(outputPlugin));
1393+
1394+
PGresult *result = NULL;
1395+
int queryResult = ExecuteOptionalRemoteCommand(connection, command->data, &result);
1396+
1397+
if (queryResult != RESPONSE_OKAY || !IsResponseOK(result) || PQntuples(result) != 1)
1398+
{
1399+
ReportResultError(connection, result, ERROR);
1400+
}
1401+
1402+
char *allowedPlugins = "";
1403+
if (!PQgetisnull(result, 0, 0))
1404+
{
1405+
allowedPlugins = pstrdup(PQgetvalue(result, 0, 0));
1406+
}
1407+
1408+
bool isAllowed = (strcmp(PQgetvalue(result, 0, 1), "t") == 0);
1409+
1410+
PQclear(result);
1411+
ForgetResults(connection);
1412+
1413+
if (isAllowed)
1414+
{
1415+
return;
1416+
}
1417+
1418+
StringInfo newValue = makeStringInfo();
1419+
if (allowedPlugins[0] != '\0')
1420+
{
1421+
appendStringInfo(newValue, "%s, ", allowedPlugins);
1422+
}
1423+
appendStringInfoString(newValue, outputPlugin);
1424+
1425+
ereport(ERROR, (errmsg("output plugin \"%s\" is not allowed on the source node",
1426+
outputPlugin),
1427+
errdetail("Non-blocking shard splits replicate data with the \"%s\" "
1428+
"logical decoding output plugin, but PostgreSQL on node "
1429+
"%s:%d only allows the plugins listed in "
1430+
"\"output_plugin_libraries\", which is set to \"%s\".",
1431+
outputPlugin, connection->hostname, connection->port,
1432+
allowedPlugins),
1433+
errhint("Allow the plugin on every node and reload the "
1434+
"configuration, for example: ALTER SYSTEM SET "
1435+
"output_plugin_libraries = %s; SELECT pg_reload_conf();",
1436+
newValue->data)));
1437+
}
1438+
1439+
13681440
/*
13691441
* SplitShard API to split a given shard (or shard group) in non-blocking fashion
13701442
* based on specified split points to a set of destination nodes.
@@ -1423,6 +1495,12 @@ NonBlockingShardSplit(SplitOperation splitOperation,
14231495
databaseName);
14241496
ClaimConnectionExclusively(sourceConnection);
14251497

1498+
/*
1499+
* Fail before creating any shards, publications or replication slots if
1500+
* the source node does not allow our output plugin.
1501+
*/
1502+
ErrorIfOutputPluginNotAllowed(sourceConnection, CITUS_SPLIT_DECODER_PLUGIN);
1503+
14261504
MultiConnection *sourceReplicationConnection =
14271505
GetReplicationConnection(sourceShardToCopyNode->workerName,
14281506
sourceShardToCopyNode->workerPort);
@@ -1493,7 +1571,7 @@ NonBlockingShardSplit(SplitOperation splitOperation,
14931571
groupedLogicalRepTargetsHash,
14941572
superUser, databaseName);
14951573

1496-
char *logicalRepDecoderPlugin = "citus";
1574+
char *logicalRepDecoderPlugin = CITUS_SPLIT_DECODER_PLUGIN;
14971575

14981576
/*
14991577
* 6) Create replication slots and keep track of their snapshot.

src/test/cdc/t/cdctestlib.pm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,16 @@ max_wal_senders = 100
142142
max_replication_slots = 100
143143
";
144144
$node->init(allows_streaming => 'logical');
145+
146+
# PostgreSQL 14.24, 15.19, 16.15, 17.11 and 18.6 restrict logical decoding
147+
# to the output plugins listed in output_plugin_libraries. Setting an
148+
# unknown GUC is fatal, so only set it when initdb wrote it out.
149+
if (open(my $conf_fh, '<', $node->data_dir . "/postgresql.conf")) {
150+
$citus_config_options = $citus_config_options .
151+
"\noutput_plugin_libraries = 'pgoutput, test_decoding, wal2json, citus'"
152+
if grep { /output_plugin_libraries/ } <$conf_fh>;
153+
close($conf_fh);
154+
}
145155
if ($node_type == $NODE_TYPE_COORDINATOR || $node_type == $NODE_TYPE_WORKER) {
146156
$node->append_conf("postgresql.conf",$citus_config_options);
147157
} else {

src/test/regress/Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,12 @@ check-split: all
281281
$(pg_regress_multi_check) --load-extension=citus \
282282
-- $(MULTI_REGRESS_OPTS) --schedule=$(citus_abs_srcdir)/split_schedule $(EXTRA_TESTS)
283283

284+
# Runs the split negative test against a cluster that deliberately does not
285+
# allow the "citus" output plugin in "output_plugin_libraries".
286+
check-split-output-plugin-denied: all
287+
CITUS_TEST_SKIP_OUTPUT_PLUGIN_ALLOWLIST=1 $(pg_regress_multi_check) --load-extension=citus \
288+
-- $(MULTI_REGRESS_OPTS) --schedule=$(citus_abs_srcdir)/split_output_plugin_denied_schedule $(EXTRA_TESTS)
289+
284290
check-failure: all
285291
$(pg_regress_multi_check) --load-extension=citus --mitmproxy \
286292
-- $(MULTI_REGRESS_OPTS) --schedule=$(citus_abs_srcdir)/failure_schedule $(EXTRA_TESTS)

src/test/regress/expected/multi_follower_dml.out

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -227,26 +227,18 @@ COPY the_table (a, b, z) FROM STDIN WITH CSV;
227227
ERROR: COPY command to Citus tables is not allowed in read-only mode
228228
DETAIL: the database is read-only
229229
HINT: All COPY commands to citus tables happen via 2PC, and 2PC requires the database to be in a writable state.
230-
\.
231-
invalid command \.
232230
COPY the_replicated_table (a, b, z) FROM STDIN WITH CSV;
233231
ERROR: writing to worker nodes is not currently allowed for replicated tables such as reference tables or hash distributed tables with replication factor greater than 1.
234232
DETAIL: the database is read-only
235233
HINT: All modifications to replicated tables happen via 2PC, and 2PC requires the database to be in a writable state.
236-
\.
237-
invalid command \.
238234
COPY reference_table (a, b, z) FROM STDIN WITH CSV;
239235
ERROR: writing to worker nodes is not currently allowed for replicated tables such as reference tables or hash distributed tables with replication factor greater than 1.
240236
DETAIL: the database is read-only
241237
HINT: All modifications to replicated tables happen via 2PC, and 2PC requires the database to be in a writable state.
242-
\.
243-
invalid command \.
244238
COPY citus_local_table (a, b, z) FROM STDIN WITH CSV;
245239
ERROR: COPY command to Citus tables is not allowed in read-only mode
246240
DETAIL: the database is read-only
247241
HINT: All COPY commands to citus tables happen via 2PC, and 2PC requires the database to be in a writable state.
248-
\.
249-
invalid command \.
250242
-- all multi-shard modifications require 2PC hence not supported
251243
INSERT INTO the_table (a, b, z) VALUES (2, 3, 4), (5, 6, 7);
252244
ERROR: cannot assign TransactionIds during recovery
@@ -299,20 +291,14 @@ COPY the_table (a, b, z) FROM STDIN WITH CSV;
299291
ERROR: COPY command to Citus tables is not allowed in read-only mode
300292
DETAIL: the database is read-only
301293
HINT: All COPY commands to citus tables happen via 2PC, and 2PC requires the database to be in a writable state.
302-
\.
303-
invalid command \.
304294
COPY reference_table (a, b, z) FROM STDIN WITH CSV;
305295
ERROR: writing to worker nodes is not currently allowed for replicated tables such as reference tables or hash distributed tables with replication factor greater than 1.
306296
DETAIL: the database is read-only
307297
HINT: All modifications to replicated tables happen via 2PC, and 2PC requires the database to be in a writable state.
308-
\.
309-
invalid command \.
310298
COPY citus_local_table (a, b, z) FROM STDIN WITH CSV;
311299
ERROR: COPY command to Citus tables is not allowed in read-only mode
312300
DETAIL: the database is read-only
313301
HINT: All COPY commands to citus tables happen via 2PC, and 2PC requires the database to be in a writable state.
314-
\.
315-
invalid command \.
316302
SELECT * FROM the_table ORDER BY a;
317303
a | b | z
318304
---------------------------------------------------------------------

src/test/regress/expected/multi_multiuser_copy.out

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,16 @@ COPY customer_copy_hash (c_custkey,c_name) FROM STDIN;
2525
-- COPY FROM as user with ALL access
2626
SET ROLE full_access;
2727
COPY customer_copy_hash (c_custkey,c_name) FROM STDIN;
28-
;
2928
RESET ROLE;
3029
-- COPY FROM as user with SELECT access, should fail
3130
SET ROLE read_access;
3231
COPY customer_copy_hash (c_custkey,c_name) FROM STDIN;
3332
ERROR: permission denied for table customer_copy_hash
34-
3 customer3
35-
\.
36-
invalid command \.
37-
;
38-
ERROR: syntax error at or near "3"
3933
RESET ROLE;
4034
-- COPY FROM as user with no access, should fail
4135
SET ROLE no_access;
4236
COPY customer_copy_hash (c_custkey,c_name) FROM STDIN;
4337
ERROR: permission denied for table customer_copy_hash
44-
4 customer4
45-
\.
46-
invalid command \.
47-
;
48-
ERROR: syntax error at or near "4"
4938
RESET ROLE;
5039
-- COPY TO as superuser
5140
COPY (SELECT * FROM customer_copy_hash ORDER BY 1) TO STDOUT;

src/test/regress/expected/pg12.out

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,12 @@ select create_distributed_table('cptest', 'id');
8888

8989
copy cptest from STDIN with csv where val < 4;
9090
ERROR: Citus does not support COPY FROM with WHERE
91-
1,6
92-
2,3
93-
3,2
94-
4,9
95-
5,4
96-
\.
97-
invalid command \.
9891
select sum(id), sum(val) from cptest;
99-
ERROR: syntax error at or near "1"
92+
sum | sum
93+
---------------------------------------------------------------------
94+
|
95+
(1 row)
96+
10097
-- CTE materialized/not materialized
10198
CREATE TABLE single_hash_repartition_first (id int, sum int, avg float);
10299
CREATE TABLE single_hash_repartition_second (id int primary key, sum int, avg float);

src/test/regress/expected/replicated_partitioned_table.out

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,6 @@ HINT: Run the query on the parent table "collections" instead.
126126
COPY collections_1 FROM STDIN;
127127
ERROR: modifications on partitions when replication factor is greater than 1 is not supported
128128
HINT: Run the query on the parent table "collections" instead.
129-
\.
130-
invalid command \.
131129
-- DDLs are not allowed
132130
CREATE INDEX index_on_partition ON collections_1(key);
133131
ERROR: modifications on partitions when replication factor is greater than 1 is not supported
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
-- Negative coverage for the "output_plugin_libraries" allowlist that PostgreSQL
2+
-- 14.24, 15.19, 16.15, 17.11 and 18.6 introduced. This test is only run by the
3+
-- check-split-output-plugin-denied target, which deliberately starts the cluster
4+
-- without adding "citus" to the allowlist.
5+
CREATE SCHEMA split_output_plugin_denied;
6+
SET search_path TO split_output_plugin_denied;
7+
SET citus.shard_count TO 2;
8+
SET citus.shard_replication_factor TO 1;
9+
SET citus.next_shard_id TO 8990000;
10+
CREATE TABLE table_to_split (id bigint PRIMARY KEY, value char);
11+
SELECT create_distributed_table('table_to_split', 'id');
12+
create_distributed_table
13+
---------------------------------------------------------------------
14+
15+
(1 row)
16+
17+
SELECT nodeid AS worker_1_node FROM pg_dist_node WHERE nodeport=:worker_1_port \gset
18+
SELECT nodeid AS worker_2_node FROM pg_dist_node WHERE nodeport=:worker_2_port \gset
19+
-- The split must be rejected up front. Terse verbosity keeps the node address
20+
-- and the current allowlist value out of the expected output.
21+
\set VERBOSITY terse
22+
SELECT citus_split_shard_by_split_points(
23+
8990000,
24+
ARRAY['-1073741826'],
25+
ARRAY[:worker_1_node, :worker_2_node],
26+
'force_logical');
27+
ERROR: output plugin "citus" is not allowed on the source node
28+
\set VERBOSITY default
29+
-- Nothing must have been created before the split was rejected.
30+
SELECT count(*) FROM pg_dist_shard WHERE logicalrelid = 'table_to_split'::regclass;
31+
count
32+
---------------------------------------------------------------------
33+
2
34+
(1 row)
35+
36+
SELECT run_command_on_workers($$SELECT count(*) FROM pg_replication_slots$$);
37+
run_command_on_workers
38+
---------------------------------------------------------------------
39+
(localhost,57637,t,0)
40+
(localhost,57638,t,0)
41+
(2 rows)
42+
43+
SELECT run_command_on_workers($$SELECT count(*) FROM pg_publication$$);
44+
run_command_on_workers
45+
---------------------------------------------------------------------
46+
(localhost,57637,t,0)
47+
(localhost,57638,t,0)
48+
(2 rows)
49+
50+
DROP SCHEMA split_output_plugin_denied CASCADE;
51+
NOTICE: drop cascades to table table_to_split

src/test/regress/pg_regress_multi.pl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,19 @@ sub should_defer_libdir_swap
584584
# Allow CREATE SUBSCRIPTION to work
585585
push(@pgOptions, "wal_level='logical'");
586586

587+
# PostgreSQL 14.24, 15.19, 16.15, 17.11 and 18.6 restrict logical decoding to
588+
# the output plugins listed in output_plugin_libraries. Citus uses "citus" for
589+
# non-blocking shard splits and "wal2json" in some CDC tests. Setting an unknown
590+
# GUC is fatal, so only set it when the installed PostgreSQL knows about it.
591+
if (!$ENV{CITUS_TEST_SKIP_OUTPUT_PLUGIN_ALLOWLIST} &&
592+
open(my $confSample, '<', catfile($sharedir, "postgresql.conf.sample")))
593+
{
594+
push(@pgOptions,
595+
"output_plugin_libraries='pgoutput, test_decoding, wal2json, citus'")
596+
if grep { /output_plugin_libraries/ } <$confSample>;
597+
close($confSample);
598+
}
599+
587600
# Faster logical replication status update so tests with logical replication
588601
# run faster
589602
push(@pgOptions, "wal_receiver_status_interval=0");

0 commit comments

Comments
 (0)