Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2efabfb
First pass at multi-cluster support for yamsql
ScottDugas Mar 30, 2026
f4683ff
Add a smoke test to make sure the multi-cluster works
ScottDugas Mar 30, 2026
b0986b2
Implement multi-cluster support in multi-server (I think)
ScottDugas Mar 31, 2026
e450ac7
Improve multi-cluster testing
ScottDugas Mar 31, 2026
b99da67
Run a separate server for each cluster so that mixed-mode still works
ScottDugas Apr 1, 2026
1b30cbd
Change InProcess to support multiple clusters
ScottDugas Apr 1, 2026
0a10f47
Just use one list for cluster files
ScottDugas Apr 1, 2026
c3477f3
Suppress close warnings
ScottDugas Apr 1, 2026
dd18386
Remove old YamlConnectionFactory.getNewConnection
ScottDugas Apr 2, 2026
68a9fbd
Add a tests to MultiServerConnectionFactoryTest that cover multiple c…
ScottDugas Apr 2, 2026
47d1f66
Encapsulate list of server+clusterFile in class
ScottDugas Apr 3, 2026
80a435a
Reuse Clusters in more places
ScottDugas Apr 3, 2026
7d047a3
Add Clusters.primary()
ScottDugas Apr 3, 2026
1ac4cda
Cleanup JDBCMultiServerConfig Clusters usage
ScottDugas Apr 3, 2026
07d0ade
Remove unnecessary constructor
ScottDugas Apr 3, 2026
e024ded
Shuffle the order of the cluster files for yaml tests
ScottDugas Apr 3, 2026
caf94fa
Fix Clusters.empty()
ScottDugas Apr 3, 2026
7c693f1
Fix javadoc
ScottDugas Apr 3, 2026
1683e92
Fix singleVersionTest
ScottDugas Apr 3, 2026
fa0d4e1
Extract helper to decrease nesting
ScottDugas Apr 3, 2026
eb54486
Address easy PR comments
ScottDugas Apr 10, 2026
c552435
Make ExternalServer.clusterFile @Nonnull
ScottDugas Apr 10, 2026
3bc08b4
Clarify Clusters javadoc
ScottDugas Apr 10, 2026
ce4969e
Add interface for things bound to a cluster
ScottDugas Apr 10, 2026
5effd12
Address PR Comments
ScottDugas Apr 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
Expand All @@ -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,
Expand All @@ -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 : "");
Expand Down Expand Up @@ -204,7 +214,6 @@ public Response execute(String database, String schema, String sql, List<Paramet
}

private RelationalConnection connect(String database, String schema, Options options) throws SQLException {
final var driver = (RelationalDriver) DriverManager.getDriver(createEmbeddedJDBCURI(database, schema));
return driver.connect(URI.create(createEmbeddedJDBCURI(database, schema)), options);
}

Expand Down Expand Up @@ -331,7 +340,6 @@ public RelationalResultSet scan(String database, String schema, String tableName
}

public TransactionalToken createTransactionalToken(String database, String schema, Options options) throws SQLException {
final var driver = (RelationalDriver) DriverManager.getDriver(createEmbeddedJDBCURI(database, schema));
RelationalConnection transactionalConnection = driver.connect(URI.create(createEmbeddedJDBCURI(database, schema)), options);
transactionalConnection.setAutoCommit(false);
return new TransactionalToken(transactionalConnection);
Expand Down Expand Up @@ -395,7 +403,7 @@ public void close() throws SQLException, RelationalException {
}
// We registered the Relational embed driver... cleanup.
if (this.registeredJDBCEmbedDriver) {
DriverManager.deregisterDriver(registeredDriver);
DriverManager.deregisterDriver(driver);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import javax.annotation.Nullable;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -73,6 +74,12 @@ public static List<String> allClusterFiles() {
return clusterFiles;
}

public static List<String> allClusterFilesInRandomOrder() {
final List<String> randomized = new ArrayList<>(clusterFiles);
Collections.shuffle(randomized);
return randomized;
}

public static String randomClusterFile() {
return clusterFiles.get(ThreadLocalRandom.current().nextInt(clusterFiles.size()));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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:
* <pre>{@code
* connect: { cluster: 1, uri: 0 }
Comment thread
ohadzeliger marked this conversation as resolved.
* }</pre>
* The uri component can be a few different things:
* <ul>
* <li>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</li>
* <li>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.</li>
* </ul>
*/
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 + "]";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -53,6 +53,11 @@ public Set<SemanticVersion> getVersionsUnderTest() {
return underlying.getVersionsUnderTest();
}

@Override
public int getAvailableClusterCount() {
return underlying.getAvailableClusterCount();
}

@Override
public boolean isMultiServer() {
return underlying.isMultiServer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <br>
* 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}.
* <ul>
* <li>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.
* </li>
* <li>Parameter 0: connects to the system tables (catalog). </li>
* <li>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
* </li>
* <li>Parameter String: connects to the defined String</li>
* <li>A map form for specifying the cluster:
* <pre>{@code
* connect: { cluster: 1, uri: 0 }
* connect: { cluster: 1 }
* }</pre>
* </li>
* </ul>
*
* @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);
}
Expand Down
Loading
Loading