# Copyright (c) 2023-2025, PostgreSQL Global Development Group # Test worker_spi module. use strict; use warnings FATAL => 'all'; use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; my $node = PostgreSQL::Test::Cluster->new('mynode'); $node->init; $node->start; note "testing dynamic bgworkers"; $node->safe_psql('postgres', 'CREATE EXTENSION worker_spi;'); # Launch one dynamic worker, then wait for its initialization to complete. # This consists in making sure that a table name "counted" is created # on a new schema whose name includes the index defined in input argument # of worker_spi_launch(). # By default, dynamic bgworkers connect to the "postgres" database with # an undefined role, falling back to the GUC defaults (or InvalidOid for # worker_spi_launch). my $result = $node->safe_psql('postgres', 'SELECT worker_spi_launch(4) IS NOT NULL;'); is($result, 't', "dynamic bgworker launched"); $node->poll_query_until( 'postgres', qq[SELECT count(*) > 0 FROM information_schema.tables WHERE table_schema = 'schema4' AND table_name = 'counted';]); $node->safe_psql('postgres', "INSERT INTO schema4.counted VALUES ('total', 0), ('delta', 1);"); # Issue a SIGHUP on the node to force the worker to loop once, accelerating # this test. $node->reload; # Wait until the worker has processed the tuple that has just been inserted. $node->poll_query_until('postgres', qq[SELECT count(*) FROM schema4.counted WHERE type = 'delta';], '0'); $result = $node->safe_psql('postgres', 'SELECT * FROM schema4.counted;'); is($result, qq(total|1), 'dynamic bgworker correctly consumed tuple data'); # Check the wait event used by the dynamic bgworker. $result = $node->poll_query_until( 'postgres', qq[SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';], qq[WorkerSpiMain]); is($result, 1, 'dynamic bgworker has reported "WorkerSpiMain" as wait event'); # Check the wait event used by the dynamic bgworker appears in pg_wait_events $result = $node->safe_psql('postgres', q[SELECT count(*) > 0 from pg_wait_events where type = 'Extension' and name = 'WorkerSpiMain';] ); is($result, 't', '"WorkerSpiMain" is reported in pg_wait_events'); note "testing bgworkers loaded with shared_preload_libraries"; # Create the database first so as the workers can connect to it when # the library is loaded. $node->safe_psql('postgres', q(CREATE DATABASE mydb;)); $node->safe_psql('postgres', q(CREATE ROLE myrole SUPERUSER LOGIN;)); $node->safe_psql('mydb', 'CREATE EXTENSION worker_spi;'); # Now load the module as a shared library. # Update max_worker_processes to make room for enough bgworkers, including # parallel workers these may spawn. $node->append_conf( 'postgresql.conf', q{ shared_preload_libraries = 'worker_spi' worker_spi.database = 'mydb' worker_spi.total_workers = 3 max_worker_processes = 32 }); $node->restart; # Check that bgworkers have been registered and launched. ok( $node->poll_query_until( 'mydb', qq[SELECT datname, count(datname), wait_event FROM pg_stat_activity WHERE backend_type = 'worker_spi' GROUP BY datname, wait_event;], 'mydb|3|WorkerSpiMain'), 'bgworkers all launched' ) or die "Timed out while waiting for bgworkers to be launched"; # Ask worker_spi to launch dynamic bgworkers with the library loaded, then # check their existence. Use IDs that do not overlap with the schemas created # by the previous workers. These ones use a new role, on different databases. my $myrole_id = $node->safe_psql('mydb', "SELECT oid FROM pg_roles where rolname = 'myrole';"); my $mydb_id = $node->safe_psql('mydb', "SELECT oid FROM pg_database where datname = 'mydb';"); my $postgresdb_id = $node->safe_psql('mydb', "SELECT oid FROM pg_database where datname = 'postgres';"); my $worker1_pid = $node->safe_psql('mydb', "SELECT worker_spi_launch(10, $mydb_id, $myrole_id);"); my $worker2_pid = $node->safe_psql('mydb', "SELECT worker_spi_launch(11, $postgresdb_id, $myrole_id);"); ok( $node->poll_query_until( 'mydb', qq[SELECT datname, usename, wait_event FROM pg_stat_activity WHERE backend_type = 'worker_spi dynamic' AND pid IN ($worker1_pid, $worker2_pid) ORDER BY datname;], qq[mydb|myrole|WorkerSpiMain postgres|myrole|WorkerSpiMain]), 'dynamic bgworkers all launched' ) or die "Timed out while waiting for dynamic bgworkers to be launched"; # Check BGWORKER_BYPASS_ALLOWCONN. $node->safe_psql('postgres', q(CREATE DATABASE noconndb ALLOW_CONNECTIONS false;)); my $noconndb_id = $node->safe_psql('mydb', "SELECT oid FROM pg_database where datname = 'noconndb';"); my $log_offset = -s $node->logfile; # worker_spi_launch() may be able to detect that the worker has been # stopped, so do not rely on safe_psql(). $node->psql('postgres', qq[SELECT worker_spi_launch(12, $noconndb_id, $myrole_id);]); $node->wait_for_log( qr/database "noconndb" is not currently accepting connections/, $log_offset); # bgworker bypasses the connection check, and can be launched. my $worker4_pid = $node->safe_psql('postgres', qq[SELECT worker_spi_launch(12, $noconndb_id, $myrole_id, '{"ALLOWCONN"}');] ); ok( $node->poll_query_until( 'postgres', qq[SELECT datname, usename, wait_event FROM pg_stat_activity WHERE backend_type = 'worker_spi dynamic' AND pid IN ($worker4_pid) ORDER BY datname;], qq[noconndb|myrole|WorkerSpiMain]), 'dynamic bgworker with BYPASS_ALLOWCONN started'); # Check BGWORKER_BYPASS_ROLELOGINCHECK. # First create a role without login access. $node->safe_psql( 'postgres', qq[ CREATE ROLE nologrole WITH NOLOGIN; GRANT CREATE ON DATABASE mydb TO nologrole; ]); my $nologrole_id = $node->safe_psql('mydb', "SELECT oid FROM pg_roles where rolname = 'nologrole';"); $log_offset = -s $node->logfile; # bgworker cannot be launched with login restriction. $node->psql('postgres', qq[SELECT worker_spi_launch(13, $mydb_id, $nologrole_id);]); $node->wait_for_log(qr/role "nologrole" is not permitted to log in/, $log_offset); # bgworker bypasses the login restriction, and can be launched. $log_offset = -s $node->logfile; my $worker5_pid = $node->safe_psql('mydb', qq[SELECT worker_spi_launch(13, $mydb_id, $nologrole_id, '{"ROLELOGINCHECK"}');] ); ok( $node->poll_query_until( 'mydb', qq[SELECT datname, usename, wait_event FROM pg_stat_activity WHERE backend_type = 'worker_spi dynamic' AND pid = $worker5_pid;], 'mydb|nologrole|WorkerSpiMain'), 'dynamic bgworker with BYPASS_ROLELOGINCHECK launched'); done_testing();