int cost_limit);
static void DataChecksumsShmemRequest(void *arg);
static bool DatabaseExists(Oid dboid);
+static void ErrorOnInvalidDatabases(void);
static List *BuildDatabaseList(void);
static List *BuildRelationList(bool temp_relations, bool include_shared);
static void FreeDatabaseList(List *dblist);
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("cost limit must be greater than zero"));
+ /*
+ * An invalid database cannot be connected to, so the worker would fail to
+ * process it, and unlike a dropped database its files stay around. Error
+ * out early with a hint rather than failing halfway through processing. A
+ * database which turns invalid after this check is handled by the
+ * launcher treating it as concurrently dropped.
+ */
+ ErrorOnInvalidDatabases();
+
StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit);
PG_RETURN_VOID();
DataChecksumState->worker_pid = InvalidPid;
LWLockRelease(DataChecksumsWorkerLock);
+ /*
+ * A worker which started but failed before reporting a result has most
+ * likely FATALed in InitPostgres. If the database was dropped, or was
+ * invalidated by a DROP DATABASE which is bound to remove its files,
+ * after we built the database list then that is the expected outcome and
+ * not an error, so apply the same heuristic as when the worker failed to
+ * start.
+ */
+ if (result == DATACHECKSUMSWORKER_FAILED && !DatabaseExists(db->dboid))
+ result = DATACHECKSUMSWORKER_DROPDB;
+
if (result == DATACHECKSUMSWORKER_ABORTED)
ereport(LOG,
errmsg("data checksums processing was aborted in database \"%s\"",
StartTransactionCommand();
+ /*
+ * DROP DATABASE holds an exclusive lock on the database from before it
+ * terminates the connections to it until it commits, so take a lock which
+ * conflicts with it to wait out a drop which is in flight. Without this
+ * we can see a database whose worker was just killed by DROP DATABASE ...
+ * WITH (FORCE) as still existing, and report a spurious failure.
+ */
+ LockSharedObject(DatabaseRelationId, dboid, 0, AccessShareLock);
+
rel = table_open(DatabaseRelationId, AccessShareLock);
ScanKeyInit(&skey,
Anum_pg_database_oid,
return found;
}
+/*
+ * ErrorOnInvalidDatabases
+ * Error out if the cluster contains an invalid database
+ *
+ * A database left invalid by an interrupted DROP DATABASE cannot be connected
+ * to, so data checksums can never be enabled in it, while its files remain on
+ * disk where checksum verification will find them. Report it to the caller
+ * so the user can drop it before retrying. Called from a normal backend, so
+ * unlike DatabaseExists we are already in a transaction.
+ *
+ * A cluster can contain more than one invalid database, but only the first one
+ * found is reported; collecting them all is not worth the complexity here. A
+ * user with several of them gets the error again for the next one after
+ * dropping the reported database, which the hint accounts for.
+ */
+static void
+ErrorOnInvalidDatabases(void)
+{
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_database pgdb = (Form_pg_database) GETSTRUCT(tup);
+
+ if (database_is_invalid_form(pgdb))
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot enable data checksums in a cluster with invalid database \"%s\"",
+ NameStr(pgdb->datname)),
+ errhint("Use DROP DATABASE to drop invalid databases."));
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+}
+
/*
* BuildDatabaseList
* Compile a list of all currently available databases in the cluster
$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
is($result, '10000', 'ensure checksummed pages can be read back');
+# Enabling checksums in a cluster which contains an invalid database left
+# behind by an interrupted DROP DATABASE must be refused.
+disable_data_checksums($node, wait => 1);
+
+$node->safe_psql('postgres', "CREATE DATABASE baddb;");
+$node->safe_psql('baddb',
+ "CREATE TABLE bad_t AS SELECT generate_series(1,100) AS a;");
+
+# Mark the database invalid, as an interrupted DROP DATABASE would.
+$node->safe_psql('postgres',
+ "UPDATE pg_database SET datconnlimit = -2 WHERE datname = 'baddb';");
+
+# The request must fail up front with an actionable error, rather than fail
+# halfway through processing.
+my ($ret, $stdout, $stderr) =
+ $node->psql('postgres', "SELECT pg_enable_data_checksums();");
+isnt($ret, 0, 'pg_enable_data_checksums fails with an invalid database');
+like(
+ $stderr,
+ qr/invalid database "baddb"/,
+ 'error message names the invalid database');
+like(
+ $stderr,
+ qr/DROP DATABASE/,
+ 'error message hints at dropping the database');
+test_checksum_state($node, 'off');
+
+# Dropping the invalid database clears the way.
+$node->safe_psql('postgres', "DROP DATABASE baddb;");
+enable_data_checksums($node, wait => 'on');
+
+# A database dropped while processing is in progress is not an error, the
+# remaining databases are still processed.
+disable_data_checksums($node, wait => 1);
+
+$node->safe_psql('postgres', "CREATE DATABASE dropme;");
+$node->safe_psql('dropme',
+ "CREATE TABLE dropme_t AS SELECT generate_series(1,10000) AS a;");
+
+# Hold the worker in the "postgres" database by keeping a temporary table
+# around, the worker waits for pre-existing temp tables to disappear before
+# it reports the database as processed. "dropme" was created last, so it is
+# processed after "postgres" and is still untouched while we wait.
+my $bg = $node->background_psql('postgres');
+$bg->query_safe('CREATE TEMP TABLE holdme (a int);');
+
+enable_data_checksums($node);
+
+$node->poll_query_until(
+ 'postgres', qq[
+ SELECT count(*) > 0 FROM pg_stat_activity
+ WHERE backend_type = 'datachecksums worker' AND datname = 'postgres'
+ AND query LIKE 'Waiting for % temp tables to be removed']
+) or die "timed out waiting for worker to wait for temporary tables";
+
+# Verify the assumption that processing has not reached "dropme" yet, without
+# it the test would silently stop covering the concurrent drop.
+my $log = slurp_file($node->logfile);
+unlike(
+ $log,
+ qr/initiating data checksum processing in database "dropme"/,
+ 'processing has not reached the database to drop');
+
+# Not processed yet and nobody is connected to it, so this must succeed.
+$node->safe_psql('postgres', "DROP DATABASE dropme;");
+
+# Let the worker in "postgres" finish, the launcher then moves on to the
+# database which no longer exists.
+$bg->query_safe('DROP TABLE holdme;');
+$bg->quit;
+
+wait_for_checksum_state($node, 'on');
+$node->poll_query_until('postgres',
+ "SELECT count(*) = 0 "
+ . "FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksums launcher';");
+
+# Same thing with DROP DATABASE ... WITH (FORCE), which terminates the
+# checksums worker connected to the database being dropped.
+disable_data_checksums($node, wait => 1);
+
+$node->safe_psql('postgres', "CREATE DATABASE dropmeforce;");
+$node->safe_psql('dropmeforce',
+ "CREATE TABLE dropme_t AS SELECT generate_series(1,10000) AS a;");
+
+# Hold the worker inside "dropmeforce" by keeping a temporary table around
+# there.
+$bg = $node->background_psql('dropmeforce');
+$bg->query_safe('CREATE TEMP TABLE holdme (a int);');
+
+enable_data_checksums($node);
+
+$node->poll_query_until(
+ 'postgres', qq[
+ SELECT count(*) > 0 FROM pg_stat_activity
+ WHERE backend_type = 'datachecksums worker' AND datname = 'dropmeforce'
+ AND query LIKE 'Waiting for % temp tables to be removed']
+) or die "timed out waiting for worker to wait for temporary tables";
+
+# Terminates both the session holding the temp table and the checksums
+# worker connected to the database.
+$node->safe_psql('postgres', "DROP DATABASE dropmeforce WITH (FORCE);");
+$bg->{run}->finish;
+$bg->quit;
+
+wait_for_checksum_state($node, 'on');
+$node->poll_query_until('postgres',
+ "SELECT count(*) = 0 "
+ . "FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksums launcher';");
+
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
$node->stop;
+
+# The resulting cluster must also pass offline verification, proving no
+# unchecksummed files were left behind.
+command_ok(
+ [ 'pg_checksums', '--check', '-D', $node->data_dir ],
+ 'offline checksum verification passes after enable');
+
done_testing();