diff --git a/fdb-relational-server/src/main/java/com/apple/foundationdb/relational/server/FRL.java b/fdb-relational-server/src/main/java/com/apple/foundationdb/relational/server/FRL.java index 96f3fdd2d4..bc07f7b266 100644 --- a/fdb-relational-server/src/main/java/com/apple/foundationdb/relational/server/FRL.java +++ b/fdb-relational-server/src/main/java/com/apple/foundationdb/relational/server/FRL.java @@ -79,7 +79,7 @@ @API(API.Status.EXPERIMENTAL) public class FRL implements AutoCloseable { private final FdbConnection fdbDatabase; - private final RelationalDriver registeredDriver; + private final RelationalDriver driver; private boolean registeredJDBCEmbedDriver; public FRL() throws RelationalException { @@ -91,6 +91,10 @@ public FRL(@Nonnull Options options) throws RelationalException { } public FRL(@Nonnull Options options, @Nullable String clusterFile) throws RelationalException { + this(options, clusterFile, true); + } + + public FRL(@Nonnull Options options, @Nullable String clusterFile, boolean registerDriver) throws RelationalException { final FDBDatabase fdbDb = FDBDatabaseFactory.instance().getDatabase(clusterFile); final Long asyncToSyncTimeout = options.getOption(Options.Name.ASYNC_OPERATIONS_TIMEOUT_MILLIS); if (asyncToSyncTimeout > 0) { @@ -114,7 +118,7 @@ public FRL(@Nonnull Options options, @Nullable String clusterFile) throws Relati .setStoreCatalog(storeCatalog).build(); try { - this.registeredDriver = new EmbeddedRelationalDriver(RecordLayerEngine.makeEngine( + this.driver = new EmbeddedRelationalDriver(RecordLayerEngine.makeEngine( rlConfig, Collections.singletonList(fdbDb), keySpace, @@ -130,13 +134,19 @@ public FRL(@Nonnull Options options, @Nullable String clusterFile) throws Relati .setTertiarySize(options.getOption(Options.Name.PLAN_CACHE_TERTIARY_MAX_ENTRIES)) .build())); - DriverManager.registerDriver(this.registeredDriver); - this.registeredJDBCEmbedDriver = true; + if (registerDriver) { + DriverManager.registerDriver(this.driver); + this.registeredJDBCEmbedDriver = true; + } } catch (SQLException ve) { throw new RelationalException(ve); } } + public RelationalDriver getDriver() { + return driver; + } + @SuppressWarnings("AbbreviationAsWordInName") // allow JDBCURI, though perhaps we should update this to make it clearer private static String createEmbeddedJDBCURI(String database, String schema) { return EmbeddedRelationalDriver.JDBC_URL_PREFIX + database + (schema != null ? "?schema=" + schema : ""); @@ -204,7 +214,6 @@ public Response execute(String database, String schema, String sql, List allClusterFiles() { return clusterFiles; } + public static List allClusterFilesInRandomOrder() { + final List randomized = new ArrayList<>(clusterFiles); + Collections.shuffle(randomized); + return randomized; + } + public static String randomClusterFile() { return clusterFiles.get(ThreadLocalRandom.current().nextInt(clusterFiles.size())); } diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/ConnectionTarget.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/ConnectionTarget.java new file mode 100644 index 0000000000..1a19d90191 --- /dev/null +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/ConnectionTarget.java @@ -0,0 +1,70 @@ +/* + * ConnectionTarget.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2026 Apple Inc. and the FoundationDB project authors + * + * 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 + * + * http://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.apple.foundationdb.relational.yamltests; + +import javax.annotation.Nonnull; +import java.net.URI; + +/** + * A resolved connection target consisting of a URI and a cluster index. + * + *

The cluster index identifies which FDB cluster to connect to. Index 0 is the default. + * Additional clusters can be accessed in YAMSQL files using the map form + * of the {@code connect} directive: + *

{@code
+ * connect: { cluster: 1, uri: 0 }
+ * }
+ * The uri component can be a few different things: + *
    + *
  • An index, in which case {@code 0} is the catalog, and other positive numbers refer to the schemas created + * automatically with the {@code schema_template:} block
  • + *
  • A fully qualified uri such as {@code "jdbc:embed:/FRL/MCI_DB?schema=S1"} or + * {@code "jdbc:embed:/__SYS?schema=CATALOG"}. The scheme should always be {@code jdbc:embed:}, and the framework + * will update it to control whether it goes to the embedded connection, or one of the servers.
  • + *
+ */ +public class ConnectionTarget { + @Nonnull + private final URI uri; + private final int clusterIndex; + + public ConnectionTarget(@Nonnull URI uri, int clusterIndex) { + this.uri = uri; + this.clusterIndex = clusterIndex; + } + + @Nonnull + public URI getUri() { + return uri; + } + + public int getClusterIndex() { + return clusterIndex; + } + + @Override + public String toString() { + if (clusterIndex == 0) { + return uri.toString(); + } + return uri + " [cluster=" + clusterIndex + "]"; + } +} diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlConnectionFactory.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlConnectionFactory.java index ed368fe181..b3755ba14d 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlConnectionFactory.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlConnectionFactory.java @@ -33,15 +33,16 @@ */ public interface YamlConnectionFactory { /** - * Convert a connection uri into an actual connection. + * Convert a connection uri into an actual connection on a specific cluster. * * @param connectPath the path to connect to + * @param clusterIndex the cluster to connect to (0 is the default cluster) * - * @return A new {@link RelationalConnection} for the given path appropriate for this test class + * @return A new {@link RelationalConnection} for the given path on the specified cluster * - * @throws SQLException if we cannot connect + * @throws SQLException if we cannot connect or the cluster index is not supported */ - YamlConnection getNewConnection(@Nonnull URI connectPath) throws SQLException; + YamlConnection getNewConnection(@Nonnull URI connectPath, int clusterIndex) throws SQLException; /** * The versions that the connection has, other than the current code. @@ -66,4 +67,13 @@ public interface YamlConnectionFactory { default boolean isMultiServer() { return false; } + + /** + * Returns the number of clusters available for testing. + * + * @return the number of available clusters (1 means only the default cluster) + */ + default int getAvailableClusterCount() { + return 1; + } } diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlConnectionFactoryWithOptions.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlConnectionFactoryWithOptions.java index ff96fb2223..e54ab3361d 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlConnectionFactoryWithOptions.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlConnectionFactoryWithOptions.java @@ -42,8 +42,8 @@ public YamlConnectionFactoryWithOptions(@Nonnull final YamlConnectionFactory und } @Override - public YamlConnection getNewConnection(@Nonnull final URI connectPath) throws SQLException { - final var connection = underlying.getNewConnection(connectPath); + public YamlConnection getNewConnection(@Nonnull final URI connectPath, int clusterIndex) throws SQLException { + final var connection = underlying.getNewConnection(connectPath, clusterIndex); connection.setConnectionOptions(options); return connection; } @@ -53,6 +53,11 @@ public Set getVersionsUnderTest() { return underlying.getVersionsUnderTest(); } + @Override + public int getAvailableClusterCount() { + return underlying.getAvailableClusterCount(); + } + @Override public boolean isMultiServer() { return underlying.isMultiServer(); diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlExecutionContext.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlExecutionContext.java index ade3113c7e..1f5475dddf 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlExecutionContext.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlExecutionContext.java @@ -286,29 +286,50 @@ public void registerConnectionURI(@Nonnull YamlReference.YamlResource resource, } /** - * Infers the URI of the database to which a block should connect to. + * Infers the connection target (URI and cluster index) for a block. *
- * A block can declare a connection in multiple ways: - * 1. no explicit declaration: Try to connect to the only registered connection URI in the local {@link YamlReference.YamlResource}. + *
    + *
  • no explicit declaration: Try to connect to the only registered connection URI in the local {@link YamlReference.YamlResource}. * If not, try to connect to the only connection across all parent resources. * A URI can be registered by defining a "schema_template" block before that, which sets up the database and schema for a provided schema template. - * 2. Parameter 0: connects to the system tables (catalog). - * 3. Parameter One-based Number: connects to the registered connection URI, number denotes the sequence of definitions in the local YamlResource. + *
  • + *
  • Parameter 0: connects to the system tables (catalog).
  • + *
  • Parameter One-based Number: connects to the registered connection URI, number denotes the sequence of definitions in the local YamlResource. * To access parent connection URIs, this number should be prepended by `(global)` tag. - * 4. Parameter String: connects to the defined String + *
  • + *
  • Parameter String: connects to the defined String
  • + *
  • A map form for specifying the cluster: + *
    {@code
    +     * connect: { cluster: 1, uri: 0 }
    +     * connect: { cluster: 1 }
    +     * }
    + *
  • + *
* - * @param connectObject can be {@code null}, an {@link Integer} value or a {@link String}. + * @param connectObject can be {@code null}, an {@link Integer}, a {@link String}, or a {@link Map} with + * optional {@code cluster} and {@code uri} keys. * - * @return a valid connection URI + * @return a valid connection target */ - public URI inferConnectionURI(@Nonnull final YamlReference.YamlResource resource, @Nullable Object connectObject) { + public ConnectionTarget inferConnectionTarget(@Nonnull final YamlReference.YamlResource resource, @Nullable Object connectObject) { Assert.thatUnchecked(registeredResources.contains(resource), "A YamlResource should be registered before registering available connection URIs"); + if (connectObject instanceof Map) { + final Map connectMap = CustomYamlConstructor.LinedObject.unlineKeys(Matchers.map(connectObject, "connect")); + final int clusterIndex = connectMap.containsKey("cluster") + ? ((Number) connectMap.get("cluster")).intValue() : 0; + final Object uriSpec = connectMap.getOrDefault("uri", null); + return new ConnectionTarget(resolveConnectionURI(resource, uriSpec), clusterIndex); + } + return new ConnectionTarget(resolveConnectionURI(resource, connectObject), 0); + } + + private URI resolveConnectionURI(@Nonnull final YamlReference.YamlResource resource, @Nullable Object connectObject) { if (connectObject == null) { return getConnectionFromConnectionURIList(resource, true, -1, true); } else if (connectObject instanceof Integer) { return getConnectionFromConnectionURIList(resource, false, (Integer) connectObject, false); } else { - final var stringURI = Matchers.string(connectObject); + final var stringURI = Matchers.string(connectObject, "connection object"); if (stringURI.startsWith("(global)")) { return getConnectionFromConnectionURIList(resource, false, Integer.parseInt(stringURI.substring(8).trim()), true); } diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlTestExtension.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlTestExtension.java index 673dc8a002..31662cd74d 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlTestExtension.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlTestExtension.java @@ -20,6 +20,7 @@ package com.apple.foundationdb.relational.yamltests; +import com.apple.foundationdb.annotation.API; import com.apple.foundationdb.relational.yamltests.configs.CorrectExplains; import com.apple.foundationdb.relational.yamltests.configs.CorrectExplainsAndMetrics; import com.apple.foundationdb.relational.yamltests.configs.CorrectMetrics; @@ -30,6 +31,7 @@ import com.apple.foundationdb.relational.yamltests.configs.JDBCMultiServerConfig; import com.apple.foundationdb.relational.yamltests.configs.ShowPlanOnDiff; import com.apple.foundationdb.relational.yamltests.configs.YamlTestConfig; +import com.apple.foundationdb.relational.yamltests.connectionfactory.Clusters; import com.apple.foundationdb.relational.yamltests.server.ExternalServer; import com.apple.foundationdb.test.FDBTestEnvironment; import com.google.common.collect.Iterables; @@ -65,41 +67,53 @@ public class YamlTestExtension implements TestTemplateInvocationContextProvider, private static final Logger logger = LogManager.getLogger(YamlTestExtension.class); private List testConfigs; private List maintainConfigs; - private List servers; + /** External servers grouped by jar version. Each {@link Clusters} has one server per cluster file (same order as {@link #clusterFiles}). */ @Nullable - private final String clusterFile; + private List> externalServerGroups; + @Nonnull + private final List clusterFiles; private final boolean includeMethodInDescriptions; @SuppressWarnings("unused") // Used implicitly with @ExtendWith(YamlTestExtension.class) public YamlTestExtension() { - this(FDBTestEnvironment.randomClusterFile(), false); + this(FDBTestEnvironment.allClusterFilesInRandomOrder(), false); } /** * Create a new extension with some configuration. - * @param clusterFile a custom cluster file to use, or {@code null} to inherit it from the environment, namely - * {@code FDB_CLUSTER_FILE}. + * @param clusterFile a custom cluster file to use as the primary. * @param includeMethodInDescriptions Set this to {@code true} if publishing test results to something that cannot * handle complex test hierarchies. In the record layer we maintain the full hierarchy in the output, so this is not * necessary, but if integrating some other tools this might be necessary. */ - public YamlTestExtension(@Nullable final String clusterFile, final boolean includeMethodInDescriptions) { - this.clusterFile = clusterFile; + @API(API.Status.DEPRECATED) + public YamlTestExtension(@Nonnull final String clusterFile, final boolean includeMethodInDescriptions) { + this(List.of(clusterFile), includeMethodInDescriptions); + } + + /** + * Create a new extension with an explicit list of cluster files. + * @param clusterFiles the cluster files to use, where index 0 is the primary cluster + * @param includeMethodInDescriptions Set this to {@code true} if publishing test results to something that cannot + * handle complex test hierarchies. + */ + public YamlTestExtension(@Nonnull final List clusterFiles, final boolean includeMethodInDescriptions) { + this.clusterFiles = clusterFiles; this.includeMethodInDescriptions = includeMethodInDescriptions; } @Override public void beforeAll(final ExtensionContext context) throws Exception { maintainConfigs = List.of( - new CorrectExplains(new EmbeddedConfig(clusterFile)), - new CorrectMetrics(new EmbeddedConfig(clusterFile)), - new CorrectExplainsAndMetrics(new EmbeddedConfig(clusterFile)), - new ShowPlanOnDiff(new EmbeddedConfig(clusterFile)) + new CorrectExplains(new EmbeddedConfig(clusterFiles)), + new CorrectMetrics(new EmbeddedConfig(clusterFiles)), + new CorrectExplainsAndMetrics(new EmbeddedConfig(clusterFiles)), + new ShowPlanOnDiff(new EmbeddedConfig(clusterFiles)) ); if (Boolean.parseBoolean(System.getProperty("tests.runQuick", "false"))) { - testConfigs = List.of(new EmbeddedConfig(clusterFile)); + testConfigs = List.of(new EmbeddedConfig(clusterFiles)); } else if (Boolean.parseBoolean(System.getProperty("tests.runRPC", "false"))) { - testConfigs = List.of(new JDBCInProcessConfig(clusterFile)); + testConfigs = List.of(new JDBCInProcessConfig(clusterFiles)); } else { List jars = ExternalServer.getAvailableServers(); // Fail the test if there are no available servers. This would force the execution in "runQuick" mode in case @@ -107,11 +121,22 @@ public void beforeAll(final ExtensionContext context) throws Exception { // Potentially, we can relax this a little if all tests are disabled for multi-server execution, but this is // not a likely scenario. Assertions.assertFalse(jars.isEmpty(), "There are no external servers available to run"); - servers = new ArrayList<>(); + externalServerGroups = new ArrayList<>(); + List allExternalServers = new ArrayList<>(); for (File jar : jars) { - servers.add(new ExternalServer(jar, clusterFile)); + Clusters group = Clusters.fromClusterFiles(clusterFiles, cf -> { + try { + return new ExternalServer(jar, cf); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + externalServerGroups.add(group); + for (ExternalServer server : group) { + allExternalServers.add(server); + } } - ExternalServer.startMultiple(servers); + ExternalServer.startMultiple(allExternalServers); final boolean mixedModeOnly = Boolean.parseBoolean(System.getProperty("tests.mixedModeOnly", "false")); final boolean singleExternalVersionOnly = Boolean.parseBoolean(System.getProperty("tests.singleVersion", "false")); Stream localTestingConfigs = localConfigs(mixedModeOnly, singleExternalVersionOnly); @@ -132,25 +157,25 @@ private Stream localConfigs(final boolean mixedModeOnly, final b if (mixedModeOnly || singleExternalVersionOnly) { return Stream.of(); } else { - return Stream.of(new EmbeddedConfig(clusterFile), new JDBCInProcessConfig(clusterFile)); + return Stream.of(new EmbeddedConfig(clusterFiles), new JDBCInProcessConfig(clusterFiles)); } } private Stream externalServerConfigs(final boolean singleExternalVersionOnly) { if (singleExternalVersionOnly) { - return servers.stream() - // Create an ExternalServer config with two servers of the same version for each server + return externalServerGroups.stream() + // Create an ExternalServer config with two connections to the same servers for each version // (with and without forced continuations) - .flatMap(server -> - Stream.of(new ExternalMultiServerConfig(0, server, server), - new ForceContinuations(new ExternalMultiServerConfig(0, server, server)))); + .flatMap(group -> + Stream.of(new ExternalMultiServerConfig(0, group, group), + new ForceContinuations(new ExternalMultiServerConfig(0, group, group)))); } else { - return servers.stream().flatMap(server -> - // (4 configs for each server available) - Stream.of(new JDBCMultiServerConfig(0, server, clusterFile), - new ForceContinuations(new JDBCMultiServerConfig(0, server, clusterFile)), - new JDBCMultiServerConfig(1, server, clusterFile), - new ForceContinuations(new JDBCMultiServerConfig(1, server, clusterFile)))); + return externalServerGroups.stream().flatMap(group -> + // (4 configs for each server version available) + Stream.of(new JDBCMultiServerConfig(0, group), + new ForceContinuations(new JDBCMultiServerConfig(0, group)), + new JDBCMultiServerConfig(1, group), + new ForceContinuations(new JDBCMultiServerConfig(1, group)))); } } @@ -171,22 +196,29 @@ public void afterAll(final ExtensionContext context) throws Exception { return e; } }).filter(Objects::nonNull).findFirst(); - if (servers != null) { - for (ExternalServer server : servers) { - try { - server.stop(); - } catch (Exception ex) { - if (logger.isWarnEnabled()) { - logger.warn("Failed to stop server " + server.getVersion() + " on " + server.getPort()); - } + if (externalServerGroups != null) { + for (Clusters group : externalServerGroups) { + for (ExternalServer server : group) { + stopServerSafely(server); } } + externalServerGroups = null; } if (exception.isPresent()) { throw exception.get(); } } + private static void stopServerSafely(final ExternalServer server) { + try { + server.stop(); + } catch (Exception ex) { + if (logger.isWarnEnabled()) { + logger.warn("Failed to stop server " + server.getVersion() + " on " + server.getPort()); + } + } + } + @Override public boolean supportsTestTemplate(final ExtensionContext context) { return true; // TODO check type diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/ConnectedBlock.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/ConnectedBlock.java index 5faf7e8b68..575e2d829a 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/ConnectedBlock.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/ConnectedBlock.java @@ -20,6 +20,7 @@ package com.apple.foundationdb.relational.yamltests.block; +import com.apple.foundationdb.relational.yamltests.ConnectionTarget; import com.apple.foundationdb.relational.yamltests.YamlReference; import com.apple.foundationdb.relational.yamltests.YamlConnection; import com.apple.foundationdb.relational.yamltests.YamlConnectionFactory; @@ -28,7 +29,6 @@ import org.apache.logging.log4j.Logger; import javax.annotation.Nonnull; -import java.net.URI; import java.sql.SQLException; import java.util.Collection; import java.util.List; @@ -54,14 +54,14 @@ public abstract class ConnectedBlock extends ReferencedBlock implements Block { @Nonnull YamlExecutionContext executionContext; @Nonnull - private final URI connectionURI; + private final ConnectionTarget connectionTarget; @Nonnull final List> executables; - ConnectedBlock(@Nonnull YamlReference reference, @Nonnull List> executables, @Nonnull URI connectionURI, @Nonnull YamlExecutionContext executionContext) { + ConnectedBlock(@Nonnull YamlReference reference, @Nonnull List> executables, @Nonnull ConnectionTarget connectionTarget, @Nonnull YamlExecutionContext executionContext) { super(reference); this.executables = executables; - this.connectionURI = connectionURI; + this.connectionTarget = connectionTarget; this.executionContext = executionContext; } @@ -75,14 +75,14 @@ protected final void executeExecutables(@Nonnull Collection consumer) { - logger.debug("🚠 Connecting to database: `{}`", connectionURI); - try (var connection = executionContext.getConnectionFactory().getNewConnection(connectionURI)) { - logger.debug("✅ Connected to database: `{}`", connectionURI); + logger.debug("🚠 Connecting to database: `{}`", connectionTarget); + try (var connection = executionContext.getConnectionFactory().getNewConnection(connectionTarget.getUri(), connectionTarget.getClusterIndex())) { + logger.debug("✅ Connected to database: `{}`", connectionTarget); consumer.accept(connection); } catch (SQLException sqle) { throw YamlExecutionContext.wrapContext(sqle, - () -> String.format(Locale.ROOT, "‼️ Error connecting to the database `%s` in block at %s", connectionURI, getReference()), - "connection [" + connectionURI + "]", getReference()); + () -> String.format(Locale.ROOT, "‼️ Error connecting to the database `%s` in block at %s", connectionTarget, getReference()), + "connection [" + connectionTarget + "]", getReference()); } } diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/PreambleBlock.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/PreambleBlock.java index 8bf605d2f3..40d175fe12 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/PreambleBlock.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/PreambleBlock.java @@ -42,6 +42,7 @@ public class PreambleBlock extends SupportBlock { public static final String OPTIONS = "options"; public static final String PREAMBLE_BLOCK_SUPPORTED_VERSION = "supported_version"; + public static final String PREAMBLE_BLOCK_REQUIRED_CLUSTERS = "required_clusters"; public static final String PREAMBLE_BLOCK_CONNECTION_OPTIONS = "connection_options"; private static final Logger logger = LogManager.getLogger(PreambleBlock.class); @@ -62,6 +63,15 @@ public static List parse(@Nonnull final Object document, @Nonnull final Y } Assumptions.assumeTrue(check.isSupported(), check.getMessage()); } + + // read the required_clusters option, and skip the test if not enough clusters are available. + if (optionsMap.containsKey(PREAMBLE_BLOCK_REQUIRED_CLUSTERS)) { + final int requiredClusters = ((Number) optionsMap.get(PREAMBLE_BLOCK_REQUIRED_CLUSTERS)).intValue(); + final int availableClusters = executionContext.getConnectionFactory().getAvailableClusterCount(); + Assumptions.assumeTrue(availableClusters >= requiredClusters, + "Test requires " + requiredClusters + " clusters but only " + availableClusters + " available"); + } + var connectionOptions = Options.none(); if (optionsMap.containsKey(PREAMBLE_BLOCK_CONNECTION_OPTIONS)) { connectionOptions = TestBlock.TestBlockOptions.parseConnectionOptions(Matchers.map(optionsMap.get(PREAMBLE_BLOCK_CONNECTION_OPTIONS))); diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/SetupBlock.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/SetupBlock.java index d6a0b03d4f..b56aec4d02 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/SetupBlock.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/SetupBlock.java @@ -30,8 +30,9 @@ import com.apple.foundationdb.relational.yamltests.command.Command; import com.apple.foundationdb.relational.yamltests.command.QueryCommand; +import com.apple.foundationdb.relational.yamltests.ConnectionTarget; + import javax.annotation.Nonnull; -import java.net.URI; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; @@ -56,9 +57,9 @@ public class SetupBlock extends ConnectedBlock { public static final String SETUP_BLOCK = "setup"; - protected SetupBlock(@Nonnull YamlReference reference, @Nonnull List> executables, @Nonnull URI connectionURI, + protected SetupBlock(@Nonnull YamlReference reference, @Nonnull List> executables, @Nonnull ConnectionTarget connectionTarget, @Nonnull YamlExecutionContext executionContext) { - super(reference, executables, connectionURI, executionContext); + super(reference, executables, connectionTarget, executionContext); } @Override @@ -99,16 +100,16 @@ public static List parse(@Nonnull YamlReference reference, @Nonnull Objec final var resolvedCommand = Objects.requireNonNull(Command.parse(reference.getResource(), List.of(step), "unnamed-setup-block", executionContext)); executables.add(createSetupExecutable(resolvedCommand, connectionOptions)); } - return List.of(new ManualSetupBlock(reference, executables, executionContext.inferConnectionURI(reference.getResource(), setupMap.getOrDefault(BLOCK_CONNECT, null)), + return List.of(new ManualSetupBlock(reference, executables, executionContext.inferConnectionTarget(reference.getResource(), setupMap.getOrDefault(BLOCK_CONNECT, null)), executionContext)); } catch (Throwable e) { throw YamlExecutionContext.wrapContext(e, () -> "‼️ Error parsing the setup block at " + reference, SETUP_BLOCK, reference); } } - private ManualSetupBlock(@Nonnull YamlReference reference, @Nonnull List> executables, @Nonnull URI connectionURI, + private ManualSetupBlock(@Nonnull YamlReference reference, @Nonnull List> executables, @Nonnull ConnectionTarget connectionTarget, @Nonnull YamlExecutionContext executionContext) { - super(reference, executables, connectionURI, executionContext); + super(reference, executables, connectionTarget, executionContext); } @Nonnull @@ -159,7 +160,7 @@ public static List parse(@Nonnull final YamlReference reference, @Nonnull private SchemaTemplateBlock(@Nonnull final YamlReference reference, @Nonnull final String schemaTemplateName, @Nonnull final String databaseName, @Nonnull List> executables, @Nonnull YamlExecutionContext executionContext) { - super(reference, executables, executionContext.inferConnectionURI(reference.getResource(), 0), executionContext); + super(reference, executables, executionContext.inferConnectionTarget(reference.getResource(), 0), executionContext); this.finalizingBlocks = List.of(DestructTemplateBlock.withDatabaseAndSchema(reference, executionContext, schemaTemplateName, databaseName)); } @@ -192,7 +193,7 @@ public static DestructTemplateBlock withDatabaseAndSchema(@Nonnull final YamlRef } private DestructTemplateBlock(@Nonnull final YamlReference reference, @Nonnull List> executables, @Nonnull YamlExecutionContext executionContext) { - super(reference, executables, executionContext.inferConnectionURI(reference.getResource(), 0), executionContext); + super(reference, executables, executionContext.inferConnectionTarget(reference.getResource(), 0), executionContext); } } } diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/TestBlock.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/TestBlock.java index bd60d3b626..71404a850a 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/TestBlock.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/block/TestBlock.java @@ -22,6 +22,7 @@ import com.apple.foundationdb.relational.api.Options; import com.apple.foundationdb.relational.util.Assert; +import com.apple.foundationdb.relational.yamltests.ConnectionTarget; import com.apple.foundationdb.relational.yamltests.CustomYamlConstructor; import com.apple.foundationdb.relational.yamltests.Matchers; import com.apple.foundationdb.relational.yamltests.YamlReference; @@ -42,7 +43,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.net.URI; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; @@ -400,7 +400,7 @@ public static List parse(int blockNumber, @Nonnull final YamlReference re Assert.thatUnchecked(!executables.isEmpty(), "‼️ Test block at " + reference + " have no tests to execute"); return ImmutableList.of(new TestBlock(reference, blockName, queryCommands, executables, executableTestsWithCacheCheck, - executionContext.inferConnectionURI(reference.getResource(), testsMap.getOrDefault(BLOCK_CONNECT, null)), options, executionContext)); + executionContext.inferConnectionTarget(reference.getResource(), testsMap.getOrDefault(BLOCK_CONNECT, null)), options, executionContext)); } catch (Throwable e) { throw executionContext.wrapContext(e, () -> "‼️ Error parsing the test block at " + reference, TEST_BLOCK, reference); } @@ -408,9 +408,9 @@ public static List parse(int blockNumber, @Nonnull final YamlReference re private TestBlock(@Nonnull final YamlReference reference, @Nonnull final String blockName, @Nonnull final List queryCommands, @Nonnull final List> executables, - @Nonnull final List> executableTestsWithCacheCheck, @Nonnull final URI connectionURI, + @Nonnull final List> executableTestsWithCacheCheck, @Nonnull final ConnectionTarget connectionTarget, @Nonnull final TestBlockOptions options, @Nonnull final YamlExecutionContext executionContext) { - super(reference, executables, connectionURI, executionContext); + super(reference, executables, connectionTarget, executionContext); this.blockName = blockName; this.queryCommands = queryCommands; this.options = options; diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/EmbeddedConfig.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/EmbeddedConfig.java index 36f1fa6c5e..d3c50b6b77 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/EmbeddedConfig.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/EmbeddedConfig.java @@ -20,28 +20,42 @@ package com.apple.foundationdb.relational.yamltests.configs; +import com.apple.foundationdb.annotation.API; import com.apple.foundationdb.relational.api.Options; +import com.apple.foundationdb.relational.api.exceptions.RelationalException; import com.apple.foundationdb.relational.server.FRL; import com.apple.foundationdb.relational.yamltests.YamlConnectionFactory; import com.apple.foundationdb.relational.yamltests.YamlExecutionContext; +import com.apple.foundationdb.relational.yamltests.connectionfactory.Clusters; import com.apple.foundationdb.relational.yamltests.connectionfactory.EmbeddedYamlConnectionFactory; import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; /** * Run directly against an instance of {@link FRL}. + *

+ * Starts one FRL per cluster file so that multi-cluster tests + * (using {@code connect: { cluster: N }}) can route to the correct cluster. */ public class EmbeddedConfig implements YamlTestConfig { - private FRL frl; - @Nullable - private final String clusterFile; + @Nonnull + private final List clusterFiles; + @Nonnull + private Clusters> clusters = Clusters.empty(); - public EmbeddedConfig(@Nullable final String clusterFile) { - this.clusterFile = clusterFile; + @API(API.Status.DEPRECATED) + public EmbeddedConfig(@Nonnull final String clusterFile) { + this(List.of(clusterFile)); + } + + public EmbeddedConfig(@Nonnull final List clusterFiles) { + this.clusterFiles = clusterFiles; } @Override + @SuppressWarnings("PMD.CloseResource") // FRLs are tracked in the list and closed in afterAll() public void beforeAll() throws Exception { var options = Options.builder() .withOption(Options.Name.PLAN_CACHE_PRIMARY_TIME_TO_LIVE_MILLIS, 3_600_000L) @@ -49,24 +63,37 @@ public void beforeAll() throws Exception { .withOption(Options.Name.PLAN_CACHE_TERTIARY_TIME_TO_LIVE_MILLIS, 3_600_000L) .withOption(Options.Name.PLAN_CACHE_PRIMARY_MAX_ENTRIES, 10) .build(); - frl = new FRL(options, clusterFile); + // The primary FRL registers its driver in DriverManager; additional ones do not + // We register the primary one to make sure that everything works the same if it is registered vs not, to + // the extent that is validated in the yaml test framework. + final String registeredCluster = clusterFiles.get(0); + clusters = Clusters.fromClusterFilesAsEntries(clusterFiles, + clusterFile -> { + try { + return new FRL(options, clusterFile, Objects.equals(clusterFile, registeredCluster)); + } catch (RelationalException e) { + throw e.toUncheckedWrappedException(); + } + }); } @Override + @SuppressWarnings("PMD.CloseResource") // FRLs are being closed in this loop public void afterAll() throws Exception { - if (frl != null) { - frl.close(); - frl = null; + for (final Clusters.Entry cluster : clusters) { + cluster.server().close(); } + clusters = Clusters.empty(); } @Override public YamlConnectionFactory createConnectionFactory() { - return new EmbeddedYamlConnectionFactory(clusterFile); + return new EmbeddedYamlConnectionFactory(clusters.map(e -> Clusters.mapEntry(e, FRL::getDriver))); } + @Nonnull @Override - public @Nonnull YamlExecutionContext.ContextOptions getRunnerOptions() { + public YamlExecutionContext.ContextOptions getRunnerOptions() { return YamlExecutionContext.ContextOptions.EMPTY_OPTIONS; } diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/ExternalMultiServerConfig.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/ExternalMultiServerConfig.java index 05849cd1ee..24faecff4e 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/ExternalMultiServerConfig.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/ExternalMultiServerConfig.java @@ -22,9 +22,11 @@ import com.apple.foundationdb.relational.yamltests.YamlConnectionFactory; import com.apple.foundationdb.relational.yamltests.YamlExecutionContext; +import com.apple.foundationdb.relational.yamltests.connectionfactory.Clusters; import com.apple.foundationdb.relational.yamltests.connectionfactory.ExternalServerYamlConnectionFactory; import com.apple.foundationdb.relational.yamltests.connectionfactory.MultiServerConnectionFactory; import com.apple.foundationdb.relational.yamltests.server.ExternalServer; +import com.apple.foundationdb.relational.yamltests.server.SemanticVersion; import javax.annotation.Nonnull; import java.util.List; @@ -36,14 +38,18 @@ public class ExternalMultiServerConfig implements YamlTestConfig { private final int initialConnection; - private final ExternalServer server0; - private final ExternalServer server1; + @Nonnull + private final Clusters servers0; + @Nonnull + private final Clusters servers1; - public ExternalMultiServerConfig(final int initialConnection, ExternalServer server0, ExternalServer server1) { + public ExternalMultiServerConfig(final int initialConnection, + @Nonnull Clusters servers0, + @Nonnull Clusters servers1) { super(); this.initialConnection = initialConnection; - this.server0 = server0; - this.server1 = server1; + this.servers0 = servers0; + this.servers1 = servers1; } @Override @@ -59,16 +65,18 @@ public YamlConnectionFactory createConnectionFactory() { return new MultiServerConnectionFactory( MultiServerConnectionFactory.ConnectionSelectionPolicy.ALTERNATE, initialConnection, - new ExternalServerYamlConnectionFactory(server0), - List.of(new ExternalServerYamlConnectionFactory(server1))); + new ExternalServerYamlConnectionFactory(servers0), + List.of(new ExternalServerYamlConnectionFactory(servers1))); } @Override public String toString() { + final SemanticVersion version0 = servers0.getInfo(ExternalServer::getVersion); + final SemanticVersion version1 = servers1.getInfo(ExternalServer::getVersion); if (initialConnection == 0) { - return "MultiServer (" + server0.getVersion() + " then " + server1.getVersion() + ")"; + return "MultiServer (" + version0 + " then " + version1 + ")"; } else { - return "MultiServer (" + server1.getVersion() + " then " + server0.getVersion() + ")"; + return "MultiServer (" + version1 + " then " + version0 + ")"; } } diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/JDBCInProcessConfig.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/JDBCInProcessConfig.java index ed0f217b4d..d7bef4b69e 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/JDBCInProcessConfig.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/JDBCInProcessConfig.java @@ -23,48 +23,58 @@ import com.apple.foundationdb.relational.server.InProcessRelationalServer; import com.apple.foundationdb.relational.yamltests.YamlConnectionFactory; import com.apple.foundationdb.relational.yamltests.YamlExecutionContext; +import com.apple.foundationdb.relational.yamltests.connectionfactory.Clusters; import com.apple.foundationdb.relational.yamltests.connectionfactory.JDBCInProcessYamlConnectionFactory; import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import java.io.IOException; +import java.util.List; /** * Run against an embedded JDBC server. + *

+ * Starts one in-process server per cluster file so that multi-cluster tests + * (using {@code connect: { cluster: N }}) can route to the correct cluster. */ public class JDBCInProcessConfig implements YamlTestConfig { - @Nullable - private InProcessRelationalServer server; - @Nullable - private final String clusterFile; - - public JDBCInProcessConfig() { - this(null); - } + @Nonnull + private final List clusterFiles; + @Nonnull + private Clusters> clusters = Clusters.empty(); - public JDBCInProcessConfig(@Nullable final String clusterFile) { - this.clusterFile = clusterFile; + public JDBCInProcessConfig(@Nonnull final List clusterFiles) { + this.clusterFiles = clusterFiles; } @Override + @SuppressWarnings("PMD.CloseResource") // Servers are tracked in the list and closed in afterAll() public void beforeAll() throws Exception { - try { - server = new InProcessRelationalServer(clusterFile).start(); - } catch (Exception e) { - throw new RuntimeException(e); - } + clusters = Clusters.fromClusterFilesAsEntries(clusterFiles, + clusterFile -> { + try { + return new InProcessRelationalServer(clusterFile).start(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); } @Override public void afterAll() throws Exception { - if (server != null) { - server.close(); - server = null; + for (final Clusters.Entry cluster : clusters) { + cluster.server().close(); } + clusters = Clusters.empty(); } @Override public YamlConnectionFactory createConnectionFactory() { - return new JDBCInProcessYamlConnectionFactory(server, clusterFile); + return new JDBCInProcessYamlConnectionFactory(clusters); + } + + @Nonnull + protected Clusters> getClusters() { + return clusters; } @Nonnull diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/JDBCMultiServerConfig.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/JDBCMultiServerConfig.java index 1d34dfa5dc..65a57a687f 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/JDBCMultiServerConfig.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/configs/JDBCMultiServerConfig.java @@ -21,35 +21,32 @@ package com.apple.foundationdb.relational.yamltests.configs; import com.apple.foundationdb.relational.yamltests.YamlConnectionFactory; +import com.apple.foundationdb.relational.yamltests.connectionfactory.Clusters; import com.apple.foundationdb.relational.yamltests.connectionfactory.ExternalServerYamlConnectionFactory; import com.apple.foundationdb.relational.yamltests.connectionfactory.MultiServerConnectionFactory; import com.apple.foundationdb.relational.yamltests.server.ExternalServer; +import com.apple.foundationdb.relational.yamltests.server.SemanticVersion; -import javax.annotation.Nullable; +import javax.annotation.Nonnull; import java.util.List; /** * Run against an embedded JDBC driver, and an external server, alternating commands that go against each. + *

+ * Multi-cluster support (in-process servers for all cluster files) is inherited from + * {@link JDBCInProcessConfig}. The external server clusters should have one entry per cluster file + * (matching the in-process servers), so that cluster-specific connections also alternate. */ public class JDBCMultiServerConfig extends JDBCInProcessConfig { - private final ExternalServer externalServer; + @Nonnull + private final Clusters externalServers; private final int initialConnection; - public JDBCMultiServerConfig(final int initialConnection, ExternalServer externalServer) { - this(initialConnection, externalServer, null); - } - - public JDBCMultiServerConfig(final int initialConnection, ExternalServer externalServer, - @Nullable final String clusterFile) { - super(clusterFile); + public JDBCMultiServerConfig(final int initialConnection, @Nonnull Clusters externalServers) { + super(externalServers.clusterFiles()); this.initialConnection = initialConnection; - this.externalServer = externalServer; - } - - @Override - public void beforeAll() throws Exception { - super.beforeAll(); + this.externalServers = externalServers; } @Override @@ -58,15 +55,16 @@ public YamlConnectionFactory createConnectionFactory() { MultiServerConnectionFactory.ConnectionSelectionPolicy.ALTERNATE, initialConnection, super.createConnectionFactory(), - List.of(new ExternalServerYamlConnectionFactory(externalServer))); + List.of(new ExternalServerYamlConnectionFactory(externalServers))); } @Override public String toString() { + final SemanticVersion externalVersion = externalServers.getInfo(ExternalServer::getVersion); if (initialConnection == 0) { - return "MultiServer (" + super.toString() + " then " + externalServer.getVersion() + ")"; + return "MultiServer (" + super.toString() + " then " + externalVersion + ")"; } else { - return "MultiServer (" + externalServer.getVersion() + " then " + super.toString() + ")"; + return "MultiServer (" + externalVersion + " then " + super.toString() + ")"; } } } diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/Clusters.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/Clusters.java new file mode 100644 index 0000000000..e282e13fa4 --- /dev/null +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/Clusters.java @@ -0,0 +1,162 @@ +/* + * Clusters.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2026 Apple Inc. and the FoundationDB project authors + * + * 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 + * + * http://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.apple.foundationdb.relational.yamltests.connectionfactory; + +import javax.annotation.Nonnull; +import java.sql.SQLException; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * An ordered, immutable collection of cluster entries, each pairing a server (or driver) of type {@code T} + * with the cluster file it is connected to. Each server (or driver) must be identical, except that it is pointing to a + * different cluster file. Must contain at least one entry. + * + * @param the type of server or driver held by each entry + */ +public class Clusters implements Iterable { + @Nonnull + private final List entries; + + /** Private constructor to support the static {@link Clusters#empty()}. */ + private Clusters() { + this.entries = List.of(); + } + + private Clusters(@Nonnull List entries) { + if (entries.isEmpty()) { + throw new IllegalArgumentException("At least one cluster entry is required"); + } + this.entries = List.copyOf(entries); + } + + public static Clusters empty() { + return new Clusters<>(); + } + + public static Clusters fromClusterFiles(List clusterFiles, Function toServer) { + return new Clusters<>(clusterFiles.stream() + .map(toServer) + .collect(Collectors.toList())); + } + + public static Clusters> fromClusterFilesAsEntries(List clusterFiles, Function toServer) { + return fromClusterFiles(clusterFiles, + clusterFile -> new Clusters.Entry<>(toServer.apply(clusterFile), clusterFile)); + } + + public Clusters map(Function mapper) { + return new Clusters<>(entries.stream() + .map(mapper) + .collect(Collectors.toList())); + } + + public static Entry mapEntry(Clusters.Entry existing, Function mapper) { + return new Clusters.Entry(mapper.apply(existing.server), existing.clusterFile); + } + + /** + * Return a piece of information about the underlying connections. + * @param getter a function to extract information that should be the same for all clusters (since they are identical) + * @param the type of the extracted information. + * @return the information + */ + public R getInfo(Function getter) { + return entries.stream().map(getter) + .findFirst() + .orElseThrow(() -> new IndexOutOfBoundsException("No Clusters found")); + } + + /** + * Returns the cluster files in order. + */ + @Nonnull + public List clusterFiles() { + return entries.stream().map(BoundToCluster::clusterFile).collect(Collectors.toList()); + } + + /** + * Returns the number of clusters. + */ + public int size() { + return entries.size(); + } + + /** + * Returns the entry at the given cluster index, after bounds-checking. + * + * @param clusterIndex the zero-based cluster index + * @return the entry at that index + * @throws SQLException if the index is out of range + */ + @Nonnull + public T get(int clusterIndex) throws SQLException { + if (clusterIndex < 0 || clusterIndex >= entries.size()) { + throw new SQLException("Cluster index " + clusterIndex + " not available (only " + + entries.size() + " clusters configured)"); + } + return entries.get(clusterIndex); + } + + @Override + @Nonnull + public Iterator iterator() { + return entries.iterator(); + } + + /** + * A server (or driver) paired with its cluster file. + * + * @param the type of server or driver + */ + public static class Entry implements BoundToCluster { + @Nonnull + private final T server; + @Nonnull + private final String clusterFile; + + public Entry(@Nonnull T server, @Nonnull String clusterFile) { + this.server = server; + this.clusterFile = clusterFile; + } + + @Nonnull + public T server() { + return server; + } + + @Nonnull + @Override + public String clusterFile() { + return clusterFile; + } + } + + /** + * An interface for a server (or driver) and its associated cluster file. + */ + public interface BoundToCluster { + @Nonnull + String clusterFile(); + } +} diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/EmbeddedYamlConnectionFactory.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/EmbeddedYamlConnectionFactory.java index bf9b19674c..769b56f2ab 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/EmbeddedYamlConnectionFactory.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/EmbeddedYamlConnectionFactory.java @@ -20,6 +20,8 @@ package com.apple.foundationdb.relational.yamltests.connectionfactory; +import com.apple.foundationdb.relational.api.Options; +import com.apple.foundationdb.relational.api.RelationalDriver; import com.apple.foundationdb.relational.yamltests.SimpleYamlConnection; import com.apple.foundationdb.relational.yamltests.YamlConnection; import com.apple.foundationdb.relational.yamltests.YamlConnectionFactory; @@ -32,21 +34,36 @@ import java.util.Set; public class EmbeddedYamlConnectionFactory implements YamlConnectionFactory { - private final String clusterFile; + @Nonnull + private final Clusters> clusters; - public EmbeddedYamlConnectionFactory(String clusterFile) { - this.clusterFile = clusterFile; + public EmbeddedYamlConnectionFactory(@Nonnull Clusters> clusters) { + this.clusters = clusters; } @Override - public YamlConnection getNewConnection(@Nonnull URI connectPath) throws SQLException { - return new SimpleYamlConnection(DriverManager.getConnection(connectPath.toString()), - SemanticVersion.current(), "Embedded", clusterFile); + public YamlConnection getNewConnection(@Nonnull URI connectPath, int clusterIndex) throws SQLException { + final Clusters.Entry entry = clusters.get(clusterIndex); + if (clusterIndex == 0) { + // The primary cluster's driver is registered in DriverManager, so use that path + return new SimpleYamlConnection(DriverManager.getConnection(connectPath.toString()), + SemanticVersion.current(), "Embedded", entry.clusterFile()); + } + // Non-primary clusters are not registered in DriverManager, so connect via the driver directly + return new SimpleYamlConnection( + entry.server().connect(connectPath, Options.NONE), + SemanticVersion.current(), + "Embedded[cluster=" + clusterIndex + "]", + entry.clusterFile()); + } + + @Override + public int getAvailableClusterCount() { + return clusters.size(); } @Override public Set getVersionsUnderTest() { return Set.of(SemanticVersion.current()); } - } diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/ExternalServerYamlConnectionFactory.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/ExternalServerYamlConnectionFactory.java index 2de163fcec..ec47c5a130 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/ExternalServerYamlConnectionFactory.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/ExternalServerYamlConnectionFactory.java @@ -38,30 +38,40 @@ public class ExternalServerYamlConnectionFactory implements YamlConnectionFactory { private static final Logger LOG = LogManager.getLogger(ExternalServerYamlConnectionFactory.class); - private final ExternalServer externalServer; + @Nonnull + private final Clusters clusters; - public ExternalServerYamlConnectionFactory(final ExternalServer externalServer) { - this.externalServer = externalServer; + public ExternalServerYamlConnectionFactory(@Nonnull Clusters clusters) { + this.clusters = clusters; } @Override - public YamlConnection getNewConnection(@Nonnull URI connectPath) throws SQLException { - String uriStr = connectPath.toString().replaceFirst("embed:", "relational://localhost:" + externalServer.getPort()); + public YamlConnection getNewConnection(@Nonnull URI connectPath, int clusterIndex) throws SQLException { + return createConnection(connectPath, clusters.get(clusterIndex)); + } + + @Override + public int getAvailableClusterCount() { + return clusters.size(); + } + + private YamlConnection createConnection(@Nonnull URI connectPath, @Nonnull ExternalServer server) throws SQLException { + String uriStr = connectPath.toString().replaceFirst("embed:", "relational://localhost:" + server.getPort()); if (LOG.isInfoEnabled()) { LOG.info(KeyValueLogMessage.of("Rewrote connection string for external server", "original", connectPath, "rewritten", uriStr, - "version", externalServer.getVersion())); + "version", server.getVersion())); } final Connection connection = DriverManager.getConnection(uriStr); - externalServer.validateConnectionVersion(connection); - return new SimpleYamlConnection(connection, externalServer.getVersion(), externalServer.getClusterFile()); + server.validateConnectionVersion(connection); + return new SimpleYamlConnection(connection, server.getVersion(), server.clusterFile()); } @Override public Set getVersionsUnderTest() { - return Set.of(externalServer.getVersion()); + return Set.of(clusters.getInfo(ExternalServer::getVersion)); } } diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/JDBCInProcessYamlConnectionFactory.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/JDBCInProcessYamlConnectionFactory.java index 6bd680dc46..13abbcc783 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/JDBCInProcessYamlConnectionFactory.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/JDBCInProcessYamlConnectionFactory.java @@ -38,26 +38,35 @@ public class JDBCInProcessYamlConnectionFactory implements YamlConnectionFactory { private static final Logger LOG = LogManager.getLogger(JDBCInProcessYamlConnectionFactory.class); - private final InProcessRelationalServer server; - private final String clusterFile; + @Nonnull + private final Clusters> clusters; - public JDBCInProcessYamlConnectionFactory(final InProcessRelationalServer server, final String clusterFile) { - this.server = server; - this.clusterFile = clusterFile; + public JDBCInProcessYamlConnectionFactory(@Nonnull Clusters> clusters) { + this.clusters = clusters; } @Override - public YamlConnection getNewConnection(@Nonnull URI connectPath) throws SQLException { - // Add name of the in-process running server to the connectPath. - URI connectPathPlusServerName = JDBCURI.addQueryParameter(connectPath, JDBCURI.INPROCESS_URI_QUERY_SERVERNAME_KEY, server.getServerName()); + public YamlConnection getNewConnection(@Nonnull URI connectPath, int clusterIndex) throws SQLException { + final Clusters.Entry entry = clusters.get(clusterIndex); + return createConnection(connectPath, entry.server(), entry.clusterFile()); + } + + @Override + public int getAvailableClusterCount() { + return clusters.size(); + } + + private YamlConnection createConnection(@Nonnull URI connectPath, @Nonnull InProcessRelationalServer targetServer, + @Nonnull String targetClusterFile) throws SQLException { + URI connectPathPlusServerName = JDBCURI.addQueryParameter(connectPath, JDBCURI.INPROCESS_URI_QUERY_SERVERNAME_KEY, targetServer.getServerName()); String uriStr = connectPathPlusServerName.toString().replaceFirst("embed:", "relational://"); if (LOG.isInfoEnabled()) { LOG.info(KeyValueLogMessage.of("Rewrote connection string for in-process server", "original", connectPath, "rewritten", uriStr, - "server", server.getServerName())); + "server", targetServer.getServerName())); } - return new SimpleYamlConnection(DriverManager.getConnection(uriStr), SemanticVersion.current(), "JDBC In-Process", clusterFile); + return new SimpleYamlConnection(DriverManager.getConnection(uriStr), SemanticVersion.current(), "JDBC In-Process", targetClusterFile); } @Override diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/MultiServerConnectionFactory.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/MultiServerConnectionFactory.java index c0e1801ca4..0f0bde0233 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/MultiServerConnectionFactory.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/connectionfactory/MultiServerConnectionFactory.java @@ -92,12 +92,13 @@ public MultiServerConnectionFactory(@Nonnull final ConnectionSelectionPolicy con } @Override - public YamlConnection getNewConnection(@Nonnull URI connectPath) throws SQLException { + public YamlConnection getNewConnection(@Nonnull URI connectPath, int clusterIndex) throws SQLException { if (connectionSelectionPolicy == ConnectionSelectionPolicy.DEFAULT) { - return defaultFactory.getNewConnection(connectPath); + return defaultFactory.getNewConnection(connectPath, clusterIndex); } else { return new MultiServerConnection(connectionSelectionPolicy, getNextConnectionNumber(), - defaultFactory.getNewConnection(connectPath), alternateConnections(connectPath)); + defaultFactory.getNewConnection(connectPath, clusterIndex), + alternateConnections(connectPath, clusterIndex)); } } @@ -111,11 +112,16 @@ public boolean isMultiServer() { return true; } + @Override + public int getAvailableClusterCount() { + return defaultFactory.getAvailableClusterCount(); + } + @Nonnull - private List alternateConnections(URI connectPath) { + private List alternateConnections(URI connectPath, int clusterIndex) { return alternateFactories.stream().map(factory -> { try { - return factory.getNewConnection(connectPath); + return factory.getNewConnection(connectPath, clusterIndex); } catch (SQLException e) { throw new IllegalStateException("Failed to create a connection", e); } diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/server/ExternalServer.java b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/server/ExternalServer.java index 7daed914e9..562c29e9f3 100644 --- a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/server/ExternalServer.java +++ b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/server/ExternalServer.java @@ -25,6 +25,7 @@ import com.apple.foundationdb.relational.api.exceptions.RelationalException; import com.apple.foundationdb.relational.util.Assert; import com.apple.foundationdb.relational.util.BuildVersion; +import com.apple.foundationdb.relational.yamltests.connectionfactory.Clusters; import com.google.common.base.Verify; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -54,7 +55,7 @@ /** * Class to manage running an external server. */ -public class ExternalServer { +public class ExternalServer implements Clusters.BoundToCluster { private static final Logger logger = LogManager.getLogger(ExternalServer.class); public static final String EXTERNAL_SERVER_PROPERTY_NAME = "yaml_testing_external_server"; @@ -65,7 +66,7 @@ public class ExternalServer { private int httpPort; private final SemanticVersion version; private Process serverProcess; - @Nullable + @Nonnull private final String clusterFile; /** @@ -74,7 +75,7 @@ public class ExternalServer { * @param serverJar the path to the jar to run */ public ExternalServer(@Nonnull final File serverJar, - @Nullable final String clusterFile) throws IOException { + @Nonnull final String clusterFile) throws IOException { this.clusterFile = clusterFile; this.serverJar = serverJar; @@ -128,7 +129,9 @@ public SemanticVersion getVersion() { return version; } - public String getClusterFile() { + @Nonnull + @Override + public String clusterFile() { return clusterFile; } diff --git a/yaml-tests/src/test/java/CustomTagTest.java b/yaml-tests/src/test/java/CustomTagTest.java index 1735930b8f..237b6de955 100644 --- a/yaml-tests/src/test/java/CustomTagTest.java +++ b/yaml-tests/src/test/java/CustomTagTest.java @@ -29,14 +29,14 @@ import org.junit.jupiter.params.provider.MethodSource; import javax.annotation.Nonnull; +import java.util.List; import java.util.stream.Stream; /** * Tests for custom YAML tags such as {@link com.apple.foundationdb.relational.yamltests.tags.IgnoreTag}. */ class CustomTagTest { - private static final String CLUSTER_FILE = FDBTestEnvironment.randomClusterFile(); - private static final EmbeddedConfig config = new EmbeddedConfig(CLUSTER_FILE); + private static final EmbeddedConfig config = new EmbeddedConfig(List.of(FDBTestEnvironment.randomClusterFile())); @BeforeAll static void beforeAll() throws Exception { diff --git a/yaml-tests/src/test/java/IncludeBlockTest.java b/yaml-tests/src/test/java/IncludeBlockTest.java index 6c8142f4c0..ac5c581876 100644 --- a/yaml-tests/src/test/java/IncludeBlockTest.java +++ b/yaml-tests/src/test/java/IncludeBlockTest.java @@ -18,25 +18,17 @@ * limitations under the License. */ -import com.apple.foundationdb.relational.yamltests.SimpleYamlConnection; -import com.apple.foundationdb.relational.yamltests.YamlConnection; -import com.apple.foundationdb.relational.yamltests.YamlConnectionFactory; import com.apple.foundationdb.relational.yamltests.YamlExecutionContext; import com.apple.foundationdb.relational.yamltests.YamlRunner; import com.apple.foundationdb.relational.yamltests.configs.EmbeddedConfig; import com.apple.foundationdb.relational.yamltests.configs.YamlTestConfig; -import com.apple.foundationdb.relational.yamltests.server.SemanticVersion; import com.apple.foundationdb.test.FDBTestEnvironment; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import javax.annotation.Nonnull; -import java.net.URI; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.Set; +import java.util.List; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -46,10 +38,8 @@ */ public class IncludeBlockTest { - private static final SemanticVersion VERSION = SemanticVersion.parse("4.4.8.0"); - private static final YamlTestConfig config = new EmbeddedConfig(FDBTestEnvironment.randomClusterFile()); + private static final YamlTestConfig config = new EmbeddedConfig(List.of(FDBTestEnvironment.randomClusterFile())); private static final boolean CORRECT_METRICS = false; - private static final String CLUSTER_FILE = FDBTestEnvironment.randomClusterFile(); @BeforeAll static void beforeAll() throws Exception { @@ -68,21 +58,7 @@ private void doRun(String fileName) throws Exception { } else { options = YamlExecutionContext.ContextOptions.EMPTY_OPTIONS; } - new YamlRunner(fileName, createConnectionFactory(), options).run(); - } - - YamlConnectionFactory createConnectionFactory() { - return new YamlConnectionFactory() { - @Override - public YamlConnection getNewConnection(@Nonnull URI connectPath) throws SQLException { - return new SimpleYamlConnection(DriverManager.getConnection(connectPath.toString()), VERSION, CLUSTER_FILE); - } - - @Override - public Set getVersionsUnderTest() { - return Set.of(VERSION); - } - }; + new YamlRunner(fileName, config.createConnectionFactory(), options).run(); } // yamsql files that works if they are rightly included in their parent yamsql file (that is, the parent file has diff --git a/yaml-tests/src/test/java/InitialVersionTest.java b/yaml-tests/src/test/java/InitialVersionTest.java index 8e43dec66c..987dd6d48f 100644 --- a/yaml-tests/src/test/java/InitialVersionTest.java +++ b/yaml-tests/src/test/java/InitialVersionTest.java @@ -35,6 +35,7 @@ import java.net.URI; import java.sql.DriverManager; import java.sql.SQLException; +import java.util.List; import java.util.Set; import java.util.stream.Stream; @@ -46,7 +47,7 @@ public class InitialVersionTest { private static final SemanticVersion VERSION = SemanticVersion.parse("3.0.18.0"); private static final String CLUSTER_FILE = FDBTestEnvironment.randomClusterFile(); - private static final EmbeddedConfig config = new EmbeddedConfig(CLUSTER_FILE); + private static final EmbeddedConfig config = new EmbeddedConfig(List.of(CLUSTER_FILE)); @BeforeAll static void beforeAll() throws Exception { @@ -73,7 +74,7 @@ private void doRun(String testName, YamlConnectionFactory connectionFactory) thr YamlConnectionFactory createConnectionFactory() { return new YamlConnectionFactory() { @Override - public YamlConnection getNewConnection(@Nonnull URI connectPath) throws SQLException { + public YamlConnection getNewConnection(@Nonnull URI connectPath, int clusterIndex) throws SQLException { return new SimpleYamlConnection(DriverManager.getConnection(connectPath.toString()), VERSION, CLUSTER_FILE); } diff --git a/yaml-tests/src/test/java/MultiServerConnectionFactoryTest.java b/yaml-tests/src/test/java/MultiServerConnectionFactoryTest.java index b1ee176d1d..f48fbe0668 100644 --- a/yaml-tests/src/test/java/MultiServerConnectionFactoryTest.java +++ b/yaml-tests/src/test/java/MultiServerConnectionFactoryTest.java @@ -25,7 +25,6 @@ import com.apple.foundationdb.relational.yamltests.YamlConnectionFactory; import com.apple.foundationdb.relational.yamltests.connectionfactory.MultiServerConnectionFactory; import com.apple.foundationdb.relational.yamltests.server.SemanticVersion; -import com.apple.foundationdb.test.FDBTestEnvironment; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -43,7 +42,8 @@ public class MultiServerConnectionFactoryTest { private static final SemanticVersion PRIMARY_VERSION = SemanticVersion.parse("2.2.2.0"); private static final SemanticVersion ALTERNATE_VERSION = SemanticVersion.parse("1.1.1.0"); - private static final String CLUSTER_FILE = FDBTestEnvironment.randomClusterFile(); + private static final String CLUSTER_FILE = "cluster0"; + private static final String CLUSTER_FILE_1 = "cluster1"; @ParameterizedTest @CsvSource({"0", "1"}) @@ -59,20 +59,20 @@ void testDefaultPolicy(int initialConnection) throws SQLException { // just the default for the set of versions assertEquals(Set.of(PRIMARY_VERSION, ALTERNATE_VERSION), classUnderTest.getVersionsUnderTest()); - var connection = classUnderTest.getNewConnection(URI.create("Blah")); + var connection = classUnderTest.getNewConnection(URI.create("Blah"), 0); assertConnection(connection, PRIMARY_VERSION, List.of(PRIMARY_VERSION)); - assertStatement(connection.prepareStatement("SQL"), PRIMARY_VERSION); - assertStatement(connection.prepareStatement("SQL"), PRIMARY_VERSION); + assertStatement(connection.prepareStatement("SQL"), PRIMARY_VERSION, CLUSTER_FILE); + assertStatement(connection.prepareStatement("SQL"), PRIMARY_VERSION, CLUSTER_FILE); - connection = classUnderTest.getNewConnection(URI.create("Blah")); + connection = classUnderTest.getNewConnection(URI.create("Blah"), 0); assertConnection(connection, PRIMARY_VERSION, List.of(PRIMARY_VERSION)); - assertStatement(connection.prepareStatement("SQL"), PRIMARY_VERSION); - assertStatement(connection.prepareStatement("SQL"), PRIMARY_VERSION); + assertStatement(connection.prepareStatement("SQL"), PRIMARY_VERSION, CLUSTER_FILE); + assertStatement(connection.prepareStatement("SQL"), PRIMARY_VERSION, CLUSTER_FILE); - connection = classUnderTest.getNewConnection(URI.create("Blah")); + connection = classUnderTest.getNewConnection(URI.create("Blah"), 0); assertConnection(connection, PRIMARY_VERSION, List.of(PRIMARY_VERSION)); - assertStatement(connection.prepareStatement("SQL"), PRIMARY_VERSION); - assertStatement(connection.prepareStatement("SQL"), PRIMARY_VERSION); + assertStatement(connection.prepareStatement("SQL"), PRIMARY_VERSION, CLUSTER_FILE); + assertStatement(connection.prepareStatement("SQL"), PRIMARY_VERSION, CLUSTER_FILE); } @ParameterizedTest @@ -92,41 +92,41 @@ void testAlternatePolicy(int initialConnection) throws SQLException { // - Factory current connection: initial connection // - connection current connection: initial connection // - statement: initial connection (2 statements) - var connection = classUnderTest.getNewConnection(URI.create("Blah")); + var connection = classUnderTest.getNewConnection(URI.create("Blah"), 0); assertConnection(connection, initialConnectionVersion, List.of(initialConnectionVersion, otherConnectionVersion)); - assertStatement(connection.prepareStatement("SQL"), initialConnectionVersion); + assertStatement(connection.prepareStatement("SQL"), initialConnectionVersion, CLUSTER_FILE); // next statement - assertStatement(connection.prepareStatement("SQL"), otherConnectionVersion); + assertStatement(connection.prepareStatement("SQL"), otherConnectionVersion, CLUSTER_FILE); // Second run: // - Factory current connection: alternate connection // - connection current connection: alternate connection // - statement: alternate connection (2 statements) - connection = classUnderTest.getNewConnection(URI.create("Blah")); + connection = classUnderTest.getNewConnection(URI.create("Blah"), 0); assertConnection(connection, otherConnectionVersion, List.of(otherConnectionVersion, initialConnectionVersion)); - assertStatement(connection.prepareStatement("SQL"), otherConnectionVersion); + assertStatement(connection.prepareStatement("SQL"), otherConnectionVersion, CLUSTER_FILE); // next statement - assertStatement(connection.prepareStatement("SQL"), initialConnectionVersion); + assertStatement(connection.prepareStatement("SQL"), initialConnectionVersion, CLUSTER_FILE); // Third run: // - Factory current connection: initial connection // - connection current connection: initial connection // - statement: initial connection (1 statement) - connection = classUnderTest.getNewConnection(URI.create("Blah")); + connection = classUnderTest.getNewConnection(URI.create("Blah"), 0); assertConnection(connection, initialConnectionVersion, List.of(initialConnectionVersion, otherConnectionVersion)); // just one statement for this connection - assertStatement(connection.prepareStatement("SQL"), initialConnectionVersion); + assertStatement(connection.prepareStatement("SQL"), initialConnectionVersion, CLUSTER_FILE); // Fourth run: // - Factory current connection: alternate connection // - connection current connection: alternate connection // - statement: alternate connection (3 statements) - connection = classUnderTest.getNewConnection(URI.create("Blah")); + connection = classUnderTest.getNewConnection(URI.create("Blah"), 0); assertConnection(connection, otherConnectionVersion, List.of(otherConnectionVersion, initialConnectionVersion)); - assertStatement(connection.prepareStatement("SQL"), otherConnectionVersion); + assertStatement(connection.prepareStatement("SQL"), otherConnectionVersion, CLUSTER_FILE); // next statements - assertStatement(connection.prepareStatement("SQL"), initialConnectionVersion); - assertStatement(connection.prepareStatement("SQL"), otherConnectionVersion); + assertStatement(connection.prepareStatement("SQL"), initialConnectionVersion, CLUSTER_FILE); + assertStatement(connection.prepareStatement("SQL"), otherConnectionVersion, CLUSTER_FILE); } @Test @@ -143,8 +143,58 @@ void testIllegalInitialConnection() { List.of(dummyConnectionFactory(ALTERNATE_VERSION)))); } - private void assertStatement(final RelationalPreparedStatement statement, final SemanticVersion version) throws SQLException { - assertEquals("version=" + version, ((RelationalConnection)statement.getConnection()).getPath().getQuery()); + @ParameterizedTest + @CsvSource({"0", "1"}) + void testDefaultPolicyWithCluster(int initialConnection) throws SQLException { + MultiServerConnectionFactory classUnderTest = new MultiServerConnectionFactory( + MultiServerConnectionFactory.ConnectionSelectionPolicy.DEFAULT, + initialConnection, + dummyMultiClusterConnectionFactory(PRIMARY_VERSION, List.of(CLUSTER_FILE, CLUSTER_FILE_1)), + List.of(dummyMultiClusterConnectionFactory(ALTERNATE_VERSION, List.of(CLUSTER_FILE, CLUSTER_FILE_1)))); + + // Cluster 0 should use the default factory and return CLUSTER_FILE + var connection = classUnderTest.getNewConnection(URI.create("Blah"), 0); + assertConnection(connection, PRIMARY_VERSION, List.of(PRIMARY_VERSION)); + assertEquals(CLUSTER_FILE, connection.getClusterFile()); + assertStatement(connection.prepareStatement("SQL"), PRIMARY_VERSION, CLUSTER_FILE); + + // Cluster 1 should use the default factory and return CLUSTER_FILE_1 + connection = classUnderTest.getNewConnection(URI.create("Blah"), 1); + assertConnection(connection, PRIMARY_VERSION, List.of(PRIMARY_VERSION)); + assertEquals(CLUSTER_FILE_1, connection.getClusterFile()); + assertStatement(connection.prepareStatement("SQL"), PRIMARY_VERSION, CLUSTER_FILE_1); + } + + @ParameterizedTest + @CsvSource({"0", "1"}) + void testAlternatePolicyWithCluster(int initialConnection) throws SQLException { + final SemanticVersion[] versions = new SemanticVersion[] { PRIMARY_VERSION, ALTERNATE_VERSION }; + final SemanticVersion initialConnectionVersion = versions[initialConnection]; + final SemanticVersion otherConnectionVersion = versions[(initialConnection + 1) % 2]; + + MultiServerConnectionFactory classUnderTest = new MultiServerConnectionFactory( + MultiServerConnectionFactory.ConnectionSelectionPolicy.ALTERNATE, + initialConnection, + dummyMultiClusterConnectionFactory(PRIMARY_VERSION, List.of(CLUSTER_FILE, CLUSTER_FILE_1)), + List.of(dummyMultiClusterConnectionFactory(ALTERNATE_VERSION, List.of(CLUSTER_FILE, CLUSTER_FILE_1)))); + + // Cluster 0: alternation works and cluster file is CLUSTER_FILE + var connection = classUnderTest.getNewConnection(URI.create("Blah"), 0); + assertConnection(connection, initialConnectionVersion, List.of(initialConnectionVersion, otherConnectionVersion)); + assertEquals(CLUSTER_FILE, connection.getClusterFile()); + assertStatement(connection.prepareStatement("SQL"), initialConnectionVersion, CLUSTER_FILE); + assertStatement(connection.prepareStatement("SQL"), otherConnectionVersion, CLUSTER_FILE); + + // Cluster 1: alternation works and cluster file is CLUSTER_FILE_1 + connection = classUnderTest.getNewConnection(URI.create("Blah"), 1); + assertConnection(connection, otherConnectionVersion, List.of(otherConnectionVersion, initialConnectionVersion)); + assertEquals(CLUSTER_FILE_1, connection.getClusterFile()); + assertStatement(connection.prepareStatement("SQL"), otherConnectionVersion, CLUSTER_FILE_1); + assertStatement(connection.prepareStatement("SQL"), initialConnectionVersion, CLUSTER_FILE_1); + } + + private void assertStatement(final RelationalPreparedStatement statement, final SemanticVersion version, final String clusterFile) throws SQLException { + assertEquals("version=" + version + "&cluster=" + clusterFile, ((RelationalConnection)statement.getConnection()).getPath().getQuery()); } private static void assertConnection(final YamlConnection connection, final SemanticVersion initialVersion, final List expectedVersions) { @@ -153,18 +203,30 @@ private static void assertConnection(final YamlConnection connection, final Sema } YamlConnectionFactory dummyConnectionFactory(@Nonnull SemanticVersion version) { + return dummyMultiClusterConnectionFactory(version, List.of(CLUSTER_FILE)); + } + + YamlConnectionFactory dummyMultiClusterConnectionFactory(@Nonnull SemanticVersion version, @Nonnull List clusterFiles) { return new YamlConnectionFactory() { @Override - public YamlConnection getNewConnection(@Nonnull URI connectPath) throws SQLException { - // Add query string to connection so we can tell where it came from - URI newPath = URI.create(connectPath + "?version=" + version); - return new SimpleYamlConnection(dummyConnection(newPath), version, CLUSTER_FILE); + public YamlConnection getNewConnection(@Nonnull URI connectPath, int clusterIndex) throws SQLException { + if (clusterIndex < 0 || clusterIndex >= clusterFiles.size()) { + throw new SQLException("Cluster index " + clusterIndex + " not available (only " + + clusterFiles.size() + " clusters configured)"); + } + URI newPath = URI.create(connectPath + "?version=" + version + "&cluster=" + clusterFiles.get(clusterIndex)); + return new SimpleYamlConnection(dummyConnection(newPath), version, clusterFiles.get(clusterIndex)); } @Override public Set getVersionsUnderTest() { return Set.of(version); } + + @Override + public int getAvailableClusterCount() { + return clusterFiles.size(); + } }; } diff --git a/yaml-tests/src/test/java/SupportedVersionTest.java b/yaml-tests/src/test/java/SupportedVersionTest.java index 4053f60d23..6056b3aabc 100644 --- a/yaml-tests/src/test/java/SupportedVersionTest.java +++ b/yaml-tests/src/test/java/SupportedVersionTest.java @@ -35,6 +35,7 @@ import java.net.URI; import java.sql.DriverManager; import java.sql.SQLException; +import java.util.List; import java.util.Set; import java.util.stream.Stream; @@ -47,7 +48,7 @@ public class SupportedVersionTest { private static final SemanticVersion VERSION = SemanticVersion.parse("3.0.18.0"); private static final String CLUSTER_FILE = FDBTestEnvironment.randomClusterFile(); - private static final EmbeddedConfig config = new EmbeddedConfig(CLUSTER_FILE); + private static final EmbeddedConfig config = new EmbeddedConfig(List.of(CLUSTER_FILE)); @BeforeAll static void beforeAll() throws Exception { @@ -66,7 +67,7 @@ private void doRun(String fileName) throws Exception { YamlConnectionFactory createConnectionFactory() { return new YamlConnectionFactory() { @Override - public YamlConnection getNewConnection(@Nonnull URI connectPath) throws SQLException { + public YamlConnection getNewConnection(@Nonnull URI connectPath, int clusterIndex) throws SQLException { return new SimpleYamlConnection(DriverManager.getConnection(connectPath.toString()), VERSION, CLUSTER_FILE); } diff --git a/yaml-tests/src/test/java/TransactionSetupTest.java b/yaml-tests/src/test/java/TransactionSetupTest.java index bb9a5ea4d8..137f391186 100644 --- a/yaml-tests/src/test/java/TransactionSetupTest.java +++ b/yaml-tests/src/test/java/TransactionSetupTest.java @@ -18,25 +18,17 @@ * limitations under the License. */ -import com.apple.foundationdb.relational.yamltests.SimpleYamlConnection; -import com.apple.foundationdb.relational.yamltests.YamlConnection; -import com.apple.foundationdb.relational.yamltests.YamlConnectionFactory; import com.apple.foundationdb.relational.yamltests.YamlExecutionContext; import com.apple.foundationdb.relational.yamltests.YamlRunner; import com.apple.foundationdb.relational.yamltests.configs.EmbeddedConfig; import com.apple.foundationdb.relational.yamltests.configs.YamlTestConfig; -import com.apple.foundationdb.relational.yamltests.server.SemanticVersion; import com.apple.foundationdb.test.FDBTestEnvironment; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import javax.annotation.Nonnull; -import java.net.URI; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.Set; +import java.util.List; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -46,10 +38,8 @@ */ public class TransactionSetupTest { - private static final SemanticVersion VERSION = SemanticVersion.parse("4.4.8.0"); - private static final YamlTestConfig config = new EmbeddedConfig(FDBTestEnvironment.randomClusterFile()); + private static final YamlTestConfig config = new EmbeddedConfig(List.of(FDBTestEnvironment.randomClusterFile())); private static final boolean CORRECT_METRICS = false; - private static final String CLUSTER_FILE = FDBTestEnvironment.randomClusterFile(); @BeforeAll static void beforeAll() throws Exception { @@ -68,21 +58,7 @@ private void doRun(String fileName) throws Exception { } else { options = YamlExecutionContext.ContextOptions.EMPTY_OPTIONS; } - new YamlRunner(fileName, createConnectionFactory(), options).run(); - } - - YamlConnectionFactory createConnectionFactory() { - return new YamlConnectionFactory() { - @Override - public YamlConnection getNewConnection(@Nonnull URI connectPath) throws SQLException { - return new SimpleYamlConnection(DriverManager.getConnection(connectPath.toString()), VERSION, CLUSTER_FILE); - } - - @Override - public Set getVersionsUnderTest() { - return Set.of(VERSION); - } - }; + new YamlRunner(fileName, config.createConnectionFactory(), options).run(); } static Stream shouldFail() { diff --git a/yaml-tests/src/test/java/YamlIntegrationTests.java b/yaml-tests/src/test/java/YamlIntegrationTests.java index aff182c251..527555c6fa 100644 --- a/yaml-tests/src/test/java/YamlIntegrationTests.java +++ b/yaml-tests/src/test/java/YamlIntegrationTests.java @@ -222,6 +222,11 @@ public void maxRows(YamlTest.Runner runner) throws Exception { runner.runYamsql("maxRows.yamsql"); } + @TestTemplate + public void multiClusterIsolation(YamlTest.Runner runner) throws Exception { + runner.runYamsql("multi-cluster-isolation.yamsql"); + } + @TestTemplate public void indexDdl(YamlTest.Runner runner) throws Exception { runner.runYamsql("index-ddl.yamsql"); diff --git a/yaml-tests/src/test/java/com/apple/foundationdb/relational/yamltests/server/ExternalServerTest.java b/yaml-tests/src/test/java/com/apple/foundationdb/relational/yamltests/server/ExternalServerTest.java index a171bf70d7..183a5acbb5 100644 --- a/yaml-tests/src/test/java/com/apple/foundationdb/relational/yamltests/server/ExternalServerTest.java +++ b/yaml-tests/src/test/java/com/apple/foundationdb/relational/yamltests/server/ExternalServerTest.java @@ -48,7 +48,8 @@ void setUp() throws IOException { private static File getCurrentServerPath() throws IOException { final List availableServers = ExternalServer.getAvailableServers(); for (File path : availableServers) { - if (new ExternalServer(path, null).getVersion().equals(SemanticVersion.current())) { + if (new ExternalServer(path, FDBTestEnvironment.randomClusterFile()) + .getVersion().equals(SemanticVersion.current())) { return path; } } diff --git a/yaml-tests/src/test/resources/multi-cluster-isolation.yamsql b/yaml-tests/src/test/resources/multi-cluster-isolation.yamsql new file mode 100644 index 0000000000..a9988e987b --- /dev/null +++ b/yaml-tests/src/test/resources/multi-cluster-isolation.yamsql @@ -0,0 +1,108 @@ +# +# multi-cluster-isolation.yamsql +# +# This source file is part of the FoundationDB open source project +# +# Copyright 2021-2026 Apple Inc. and the FoundationDB project authors +# +# 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 +# +# http://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. + +# Verifies that data written to one cluster is not visible on another cluster. +# Requires at least 2 clusters; skipped otherwise. +--- +options: + required_clusters: 2 +--- +# Cleanup cluster 0 +setup: + connect: { cluster: 0, uri: "jdbc:embed:/__SYS?schema=CATALOG" } + steps: + - query: drop database if exists /FRL/MCI_DB + - query: drop schema template if exists MCI_TEMPLATE +--- +# Cleanup cluster 1 +setup: + connect: { cluster: 1, uri: "jdbc:embed:/__SYS?schema=CATALOG" } + steps: + - query: drop database if exists /FRL/MCI_DB + - query: drop schema template if exists MCI_TEMPLATE +--- +# Create template, database, and schema on cluster 0 +setup: + connect: { cluster: 0, uri: "jdbc:embed:/__SYS?schema=CATALOG" } + steps: + - query: create schema template MCI_TEMPLATE create table T1(ID bigint, NAME string, primary key(ID)) + - query: create database /FRL/MCI_DB + - query: create schema /FRL/MCI_DB/S1 with template MCI_TEMPLATE +--- +# Insert data on cluster 0 +setup: + connect: { cluster: 0, uri: "jdbc:embed:/FRL/MCI_DB?schema=S1" } + steps: + - query: insert into T1 values (1, 'cluster0_row1') + - query: insert into T1 values (2, 'cluster0_row2') +--- +# Verify cluster 1 does not have the database +test_block: + connect: { cluster: 1, uri: "jdbc:embed:/__SYS?schema=CATALOG" } + tests: + - + - query: select count(*) from "DATABASES" where database_id = '/FRL/MCI_DB' + - result: [{0}] +--- +# Create same template, database, and schema on cluster 1 +setup: + connect: { cluster: 1, uri: "jdbc:embed:/__SYS?schema=CATALOG" } + steps: + - query: create schema template MCI_TEMPLATE create table T1(ID bigint, NAME string, primary key(ID)) + - query: create database /FRL/MCI_DB + - query: create schema /FRL/MCI_DB/S1 with template MCI_TEMPLATE +--- +# Insert different data on cluster 1 +setup: + connect: { cluster: 1, uri: "jdbc:embed:/FRL/MCI_DB?schema=S1" } + steps: + - query: insert into T1 values (10, 'cluster1_row1') + - query: insert into T1 values (20, 'cluster1_row2') + - query: insert into T1 values (30, 'cluster1_row3') +--- +# Verify cluster 1 has only its own data +test_block: + connect: { cluster: 1, uri: "jdbc:embed:/FRL/MCI_DB?schema=S1" } + tests: + - + - query: select * from T1 + - result: [{ID: 10, NAME: 'cluster1_row1'}, {ID: 20, NAME: 'cluster1_row2'}, {ID: 30, NAME: 'cluster1_row3'}] +--- +# Verify cluster 0 still has only its own data +test_block: + connect: { cluster: 0, uri: "jdbc:embed:/FRL/MCI_DB?schema=S1" } + preset: single_repetition_ordered + tests: + - + - query: select * from T1 + - result: [{ID: 1, NAME: 'cluster0_row1'}, {ID: 2, NAME: 'cluster0_row2'}] +--- +# Cleanup cluster 0 +setup: + connect: { cluster: 0, uri: "jdbc:embed:/__SYS?schema=CATALOG" } + steps: + - query: drop database /FRL/MCI_DB + - query: drop schema template MCI_TEMPLATE +--- +# Cleanup cluster 1 +setup: + connect: { cluster: 1, uri: "jdbc:embed:/__SYS?schema=CATALOG" } + steps: + - query: drop database /FRL/MCI_DB + - query: drop schema template MCI_TEMPLATE