Generation qualification refused every Amazon Aurora PostgreSQL server:
Aurora always reports full_page_writes = off and has no parameter to change
it, because its storage layer does not write torn pages.
The settings query now also returns
to_regprocedure('aurora_version()') IS NOT NULL (true on Aurora, false on
stock PostgreSQL, never an error). full_page_writes = off is accepted only
when that is true. Stock PostgreSQL must still report it on, and fsync on,
synchronous_commit on or remote_apply and every other check are unchanged
on both. The refusal text now says full-page writes are not required on
Aurora.
Tests run on a real postgres:17 engine whose settings are really changed;
only Aurora is simulated, by creating a function named aurora_version() in
the test database.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
246 lines
11 KiB
Java
246 lines
11 KiB
Java
/*
|
|
* QQQ - Low-code Application Framework for Engineers.
|
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
|
* contact@kingsrook.com
|
|
* https://github.com/Kingsrook/
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* https://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.kingsrook.qbits.documentgeneration;
|
|
|
|
|
|
import java.sql.Connection;
|
|
import java.sql.DriverManager;
|
|
import java.util.UUID;
|
|
import com.kingsrook.qqq.backend.module.postgres.model.metadata.PostgreSQLBackendMetaData;
|
|
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
|
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
|
import org.testcontainers.containers.GenericContainer;
|
|
import org.testcontainers.containers.wait.strategy.Wait;
|
|
|
|
|
|
/*******************************************************************************
|
|
** Disposable synthetic PostgreSQL 17 engine on a dynamic loopback port. The
|
|
** superuser is fixture-only migration authority; native QQQ connections use a
|
|
** separate DML-only role with bounded lock_timeout. No shared volume or data.
|
|
*******************************************************************************/
|
|
final class PostgresGenerationDatabase implements AutoCloseable
|
|
{
|
|
private static final String IMAGE = "postgres:17";
|
|
private static final String OPTIONS = "?connectTimeout=5&socketTimeout=20&options=-c%20TimeZone%3DUTC";
|
|
private final String rootPassword = UUID.randomUUID().toString();
|
|
private final String appPassword = UUID.randomUUID().toString();
|
|
private final GenericContainer<?> container;
|
|
private String schema;
|
|
private String server;
|
|
private RDBMSBackendMetaData backend;
|
|
private final java.util.TimeZone previousZone;
|
|
|
|
/***************************************************************************
|
|
** Starts only this suite's loopback container with durable settings.
|
|
***************************************************************************/
|
|
PostgresGenerationDatabase()
|
|
{
|
|
this(true);
|
|
}
|
|
|
|
/***************************************************************************
|
|
** Pinned durability settings come from the server command line, which no
|
|
** ALTER SYSTEM can override. An unpinned engine starts with the same values
|
|
** (the PostgreSQL defaults) and lets a test weaken them through system().
|
|
***************************************************************************/
|
|
PostgresGenerationDatabase(boolean pinned)
|
|
{
|
|
// pgjdbc announces the JVM zone as the session TimeZone; qualification requires UTC.
|
|
previousZone = java.util.TimeZone.getDefault();
|
|
java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("UTC"));
|
|
container = new GenericContainer<>(IMAGE)
|
|
.withEnv("POSTGRES_PASSWORD", rootPassword).withEnv("POSTGRES_DB", "bootstrap")
|
|
.withCommand(pinned ? new String[] { "postgres", "-c", "fsync=on", "-c", "synchronous_commit=on", "-c", "full_page_writes=on", "-c", "max_connections=60" }
|
|
: new String[] { "postgres", "-c", "max_connections=60" })
|
|
.withExposedPorts(5432)
|
|
.withCreateContainerCmdModifier(command -> command.getHostConfig().withMemory(768L * 1024 * 1024).withNanoCPUs(2_000_000_000L)
|
|
.withPortBindings(FixtureEndpoint.publishedBinding(5432)))
|
|
.waitingFor(Wait.forLogMessage(".*database system is ready to accept connections.*\\s", 2).withStartupTimeout(java.time.Duration.ofMinutes(2)));
|
|
try
|
|
{
|
|
container.start();
|
|
refreshEndpoint();
|
|
try(var connection = DriverManager.getConnection(server + "bootstrap" + OPTIONS, "postgres", rootPassword); var statement = connection.createStatement())
|
|
{
|
|
statement.execute("CREATE ROLE generation_app LOGIN PASSWORD '" + appPassword + "' NOSUPERUSER NOCREATEDB NOCREATEROLE");
|
|
statement.execute("ALTER ROLE generation_app SET lock_timeout='5s'");
|
|
}
|
|
}
|
|
catch(Exception failure)
|
|
{
|
|
try
|
|
{
|
|
close();
|
|
}
|
|
catch(RuntimeException cleanup)
|
|
{
|
|
failure.addSuppressed(cleanup);
|
|
}
|
|
throw new IllegalStateException("PostgreSQL fixture failed to start", failure);
|
|
}
|
|
}
|
|
|
|
/***************************************************************************
|
|
** Every inherited test receives a fresh database and the DML-only identity.
|
|
***************************************************************************/
|
|
RDBMSBackendMetaData database(String name) throws Exception
|
|
{
|
|
schema = "generation_" + UUID.randomUUID().toString().replace("-", "");
|
|
try(var connection = DriverManager.getConnection(server + "bootstrap" + OPTIONS, "postgres", rootPassword); var statement = connection.createStatement())
|
|
{
|
|
statement.execute("CREATE DATABASE " + schema);
|
|
statement.execute("REVOKE ALL ON DATABASE " + schema + " FROM PUBLIC");
|
|
statement.execute("GRANT CONNECT ON DATABASE " + schema + " TO generation_app");
|
|
}
|
|
try(var connection = connection(); var statement = connection.createStatement())
|
|
{
|
|
statement.execute("GRANT USAGE ON SCHEMA public TO generation_app");
|
|
statement.execute("ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO generation_app");
|
|
statement.execute("ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO generation_app");
|
|
}
|
|
backend = new PostgreSQLBackendMetaData();
|
|
backend.setName(name);
|
|
backend.setDatabaseName(schema);
|
|
backend.setUsername("generation_app");
|
|
backend.setPassword(appPassword);
|
|
backend.setJdbcUrl(server + schema + OPTIONS);
|
|
return (backend);
|
|
}
|
|
|
|
/***************************************************************************
|
|
** Separate owned fixture connection for migrations/assertions/fault injection.
|
|
***************************************************************************/
|
|
Connection connection() throws Exception
|
|
{
|
|
return (DriverManager.getConnection(server + schema + OPTIONS, "postgres", rootPassword));
|
|
}
|
|
|
|
/***************************************************************************
|
|
** Translate only the known fixture-specific H2 statements, never application SQL.
|
|
***************************************************************************/
|
|
void sql(String source) throws Exception
|
|
{
|
|
if(source.equals("INSERT INTO document_generation_item (state) SELECT 'QUEUED' FROM SYSTEM_RANGE(1,10000)"))
|
|
{
|
|
source = "INSERT INTO document_generation_item (state) SELECT 'QUEUED' FROM generate_series(1,10000)";
|
|
}
|
|
try(var connection = connection(); var statement = connection.createStatement())
|
|
{
|
|
for(String sql : source.split(";"))
|
|
{
|
|
if(!sql.isBlank())
|
|
{
|
|
statement.execute(sql);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/***************************************************************************
|
|
** Change one reloadable server setting of an unpinned engine (ALTER SYSTEM and
|
|
** a configuration reload), then wait until a new session actually reports it.
|
|
** Fixed fixture names and values only, never application input.
|
|
***************************************************************************/
|
|
void system(String name, String value) throws Exception
|
|
{
|
|
ConnectionManager.closeConnectionProviders();
|
|
try(var connection = DriverManager.getConnection(server + "bootstrap" + OPTIONS, "postgres", rootPassword); var statement = connection.createStatement())
|
|
{
|
|
statement.execute("ALTER SYSTEM SET " + name + " = '" + value + "'");
|
|
statement.execute("SELECT pg_reload_conf()");
|
|
}
|
|
long deadline = System.nanoTime() + java.time.Duration.ofSeconds(30).toNanos();
|
|
while(true)
|
|
{
|
|
try(var connection = DriverManager.getConnection(server + "bootstrap" + OPTIONS, "postgres", rootPassword); var statement = connection.createStatement();
|
|
var row = statement.executeQuery("SELECT current_setting('" + name + "')"))
|
|
{
|
|
if(row.next() && value.equals(row.getString(1)))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
if(System.nanoTime() >= deadline)
|
|
{
|
|
throw new IllegalStateException("PostgreSQL fixture did not apply " + name);
|
|
}
|
|
Thread.sleep(100);
|
|
}
|
|
}
|
|
|
|
/***************************************************************************
|
|
** Cold server restart retains only this container's data layer (committed
|
|
** data must survive; Docker may publish a different loopback port).
|
|
***************************************************************************/
|
|
void restart() throws Exception
|
|
{
|
|
ConnectionManager.closeConnectionProviders();
|
|
container.getDockerClient().stopContainerCmd(container.getContainerId()).withTimeout(10).exec();
|
|
container.getDockerClient().startContainerCmd(container.getContainerId()).exec();
|
|
refreshEndpoint();
|
|
backend.setJdbcUrl(server + schema + OPTIONS);
|
|
long deadline = System.nanoTime() + java.time.Duration.ofSeconds(60).toNanos();
|
|
while(true)
|
|
{
|
|
try(var connection = connection())
|
|
{
|
|
if(connection.isValid(2))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
catch(java.sql.SQLException unavailable)
|
|
{
|
|
if(System.nanoTime() >= deadline)
|
|
{
|
|
throw unavailable;
|
|
}
|
|
}
|
|
Thread.sleep(200);
|
|
}
|
|
}
|
|
|
|
/***************************************************************************
|
|
** Native inspect avoids Testcontainers' stale mapped-port cache after restart.
|
|
***************************************************************************/
|
|
private void refreshEndpoint()
|
|
{
|
|
var address = FixtureEndpoint.of(container, 5432);
|
|
server = "jdbc:postgresql://" + address.host() + ":" + address.port() + "/";
|
|
}
|
|
|
|
/***************************************************************************
|
|
** Removes only this exact disposable test container and its anonymous volumes.
|
|
***************************************************************************/
|
|
@Override
|
|
public void close()
|
|
{
|
|
try
|
|
{
|
|
container.close();
|
|
}
|
|
finally
|
|
{
|
|
java.util.TimeZone.setDefault(previousZone);
|
|
}
|
|
}
|
|
}
|