Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
99c8a89
Add support for connecting to multiple cluster files in tests
ScottDugas Sep 8, 2025
43aa388
Add support for testing specifically against multiple clusters
ScottDugas Sep 8, 2025
c26e200
Ensure FDB gets opened even if there are no cluster files
ScottDugas Sep 8, 2025
672b305
Have fdb-extensions get cluster info from yaml too
ScottDugas Sep 8, 2025
f4aea84
Fix handling behavior when there is no cluster file specified
ScottDugas Sep 8, 2025
11c900a
Throw RecordCoreException in FDBDatabaseExtepsion setup
ScottDugas Sep 8, 2025
d15b81e
Fix up exception conversion
ScottDugas Sep 8, 2025
d31a46d
Start two fdb clusters in CI
ScottDugas Sep 8, 2025
e058377
Add snakeyaml dependency to projects depending on core tests
ScottDugas Sep 18, 2025
8e84a0e
Fix sqlline test
ScottDugas Sep 18, 2025
71db28c
Use dbExtension.getDatabase in FDBRecordContextTest
ScottDugas Sep 18, 2025
68fa77a
Create database with cluster file in ResolverMappingReplicatorTest.te…
ScottDugas Sep 18, 2025
ad284a6
Fixup RankIndexTest.writeOnlyRankQuery
ScottDugas Sep 18, 2025
f418350
Try creating an FDBTestClusterConfig in testFixtures
ScottDugas Sep 19, 2025
2b9af6d
Don't use fixtures yet
ScottDugas Sep 19, 2025
66c5f5f
Update some tests to get random cluster files from FDBTestEnvironment
ScottDugas Sep 19, 2025
9cb9f2c
Update remaining tests to not use `null` clusterFile
ScottDugas Sep 19, 2025
134938e
Fix checkstyle/javadoc checks
ScottDugas Sep 19, 2025
64deb2b
Add option to start RelationalServer with a clusterFile & use that
ScottDugas Sep 19, 2025
e48f0f3
LocatableResolverTest needs to use the same clusterFile in tests that…
ScottDugas Sep 19, 2025
74b886e
Fix last couple tests that wipe FDB
ScottDugas Sep 19, 2025
6a953b0
Fix JDBC tests that depend on creating in process server
ScottDugas Sep 19, 2025
d1c1ee1
Fixup InProcessRelationalServer to use FDBTestEnvironment
ScottDugas Sep 22, 2025
fa20608
Update yaml-tests to use FDBTestEnvironment
ScottDugas Sep 22, 2025
44037f2
Have YamlConnection provide cluster file so we can apply direct opera…
ScottDugas Sep 22, 2025
dae9a82
ExternalServerTest needs to provide a clusterFile
ScottDugas Sep 22, 2025
ffec130
Fixup comments in FDBTestEnvironment
ScottDugas Sep 22, 2025
4617fc7
Change TestDatabaseExtension to pick a random database
ScottDugas Sep 22, 2025
83f2657
Cleanup some cases where tests were using multiple clusters unintenti…
ScottDugas Sep 22, 2025
c9eeaa4
Have FDBDatabaseExtension use a single cluster for the test
ScottDugas Sep 22, 2025
1e5552f
Always provide a cluster file, and reset in afterEach
ScottDugas Sep 23, 2025
244c5f0
Properly handle null clusterFile
ScottDugas Sep 23, 2025
0b63c36
Merge branch 'main' into multiple-fdb-clusters
ScottDugas Sep 24, 2025
e4d4631
Fixup post merge
ScottDugas Sep 24, 2025
252f879
Fixup dependencies to use testFixtures.
ScottDugas Sep 24, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ protogen/

# Local FDB settings
fdb-environment.properties
fdb-environment.yaml

# Docker local support
run/
Expand Down
57 changes: 51 additions & 6 deletions actions/setup-fdb/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,57 @@ runs:
- name: Install FDB Server
shell: bash
run: sudo dpkg -i ~/.fdb-cache/${{ steps.fdb_filenames.outputs.client_deb }} ~/.fdb-cache/${{ steps.fdb_filenames.outputs.server_deb }}
- name: Fix FDB Network Addresses
- name: Stop default fdb
shell: bash
run: sudo sed -i -e "s/public_address = auto:\$ID/public_address = 127.0.0.1:\$ID/g" -e "s/listen_address = public/listen_address = 0.0.0.0:\$ID/g" /etc/foundationdb/foundationdb.conf
- name: Start FDB Server
run: sudo service foundationdb stop

- name: Create cluster1 config
shell: bash
run: |
sudo cp /etc/foundationdb/foundationdb.conf /etc/foundationdb/foundationdb1.conf

sudo sed -i -e "s/\/etc\/foundationdb\/fdb.cluster/\/etc\/foundationdb\/fdb1.cluster/g" \
-e "s/\/var\/log\/foundationdb/\/var\/log\/foundationdb1/" \
-e "s/fdbserver.4500/fdbserver.4600/g" \
/etc/foundationdb/foundationdb1.conf

sudo bash -c "echo 'fdb1:$(mktemp -u XXXXXXXX)@127.0.0.1:4600' > /etc/foundationdb/fdb1.cluster"
- name: Create cluster2 config
shell: bash
run: sudo /usr/lib/foundationdb/fdbmonitor /etc/foundationdb/foundationdb.conf --daemonize
- name: Switch FDB to SSD
run: |
sudo cp /etc/foundationdb/foundationdb.conf /etc/foundationdb/foundationdb2.conf

sudo sed -i -e "s/\/etc\/foundationdb\/fdb.cluster/\/etc\/foundationdb\/fdb2.cluster/g" \
-e "s/\/var\/log\/foundationdb/\/var\/log\/foundationdb2/" \
-e "s/fdbserver.4500/fdbserver.4700/g" \
/etc/foundationdb/foundationdb2.conf

sudo bash -c "echo 'fdb2:$(mktemp -u XXXXXXXX)@127.0.0.1:4700' > /etc/foundationdb/fdb2.cluster"

- name: create dirs & set permissions
shell: bash
run: fdbcli --exec "configure single ssd storage_migration_type=aggressive; status"
run: |
sudo mkdir /var/log/foundationdb1 /var/log/foundationdb2
sudo chown foundationdb:foundationdb /etc/foundationdb/fdb1.cluster /etc/foundationdb/fdb2.cluster /var/log/foundationdb1 /var/log/foundationdb2
sudo chmod 664 /etc/foundationdb/fdb1.cluster /etc/foundationdb/fdb2.cluster
sudo chmod 700 /var/log/foundationdb1 /var/log/foundationdb2

- name: Start FDB Server 1
shell: bash
run: sudo /usr/lib/foundationdb/fdbmonitor --conffile /etc/foundationdb/foundationdb1.conf --lockfile /var/run/fdbmonitor1.pid --loggroup fdb1 --daemonize
- name: Start FDB Server 2
shell: bash
run: sudo /usr/lib/foundationdb/fdbmonitor --conffile /etc/foundationdb/foundationdb2.conf --lockfile /var/run/fdbmonitor2.pid --loggroup fdb2 --daemonize

- name: Switch FDB 1 to SSD
shell: bash
run: fdbcli -C /etc/foundationdb/fdb1.cluster --exec "configure new single ssd storage_migration_type=aggressive; status"
- name: Switch FDB 2 to SSD
shell: bash
run: fdbcli -C /etc/foundationdb/fdb2.cluster --exec "configure new single ssd storage_migration_type=aggressive; status"
- name: Create fdb-environment.yaml
shell: bash
run: |
echo "clusterFiles: " >> fdb-environment.yaml
echo " - /etc/foundationdb/fdb1.cluster" >> fdb-environment.yaml
echo " - /etc/foundationdb/fdb2.cluster" >> fdb-environment.yaml
19 changes: 19 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
* limitations under the License.
*/

import org.yaml.snakeyaml.Yaml

import org.apache.tools.ant.taskdefs.condition.Os

buildscript {
repositories {
if (Boolean.parseBoolean(mavenLocalEnabled)) {
Expand All @@ -29,6 +33,7 @@ buildscript {

dependencies {
classpath 'org.jboss.tattletale:tattletale:1.2.0.Beta2'
classpath libs.snakeyaml
}
}

Expand Down Expand Up @@ -341,6 +346,8 @@ if (!JavaVersion.current().isJava8Compatible()) {
throw new Exception("Java 8 is required to build fdb-record-layer")
}

// fdb-environment.properties is the old way we configured the library path and cluster file, it does not scale well
// to multiple cluster files, so is being replaced with a yaml file.
def fdbEnvironmentFile = new File("${rootProject.projectDir}/fdb-environment.properties")
if (fdbEnvironmentFile.exists()) {
fdbEnvironmentFile.eachLine { line ->
Expand All @@ -356,3 +363,15 @@ if (!ext.fdbEnvironment.isEmpty()) {
}
}
}

def fdbEnvironmentYamlFile = new File("${rootProject.projectDir}/fdb-environment.yaml")
if (fdbEnvironmentYamlFile.exists()) {
def libraryPath = new Yaml().loadAll(fdbEnvironmentYamlFile.newInputStream()).first().libraryPath
def libraryPathVariable = Os.isFamily(Os.FAMILY_MAC) ? "DYLD_LIBRARY_PATH" : "LD_LIBRARY_PATH"
allprojects {
tasks.withType(Test) { task ->
task.environment([(libraryPathVariable): libraryPath])
task.environment(FDB_ENVIRONMENT_YAML: fdbEnvironmentYamlFile.absolutePath)
}
}
}
7 changes: 7 additions & 0 deletions docker-local/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ FDB_CLUSTER_FILE=${FDB_CLUSTER_FILE}
${LIBRARY_PATH}=${RUNDIR}
EOF

cat >${ROOTDIR}/fdb-environment.yaml <<EOF
# docker-local
clusterFiles:
- ${FDB_CLUSTER_FILE}
libraryPath: ${RUNDIR}
EOF

cat ${ROOTDIR}/fdb-environment.properties

echo "Docker-based FDB cluster is now up."
19 changes: 18 additions & 1 deletion docs/sphinx/source/Building.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,24 @@ If you enable the local repo in whatever uses the Record Layer, the following wi
./gradlew publishToMavenLocal -PpublishBuild=true
```

## Configuring tests for a non-standard FDB cluster file location
## Configuring tests for a non-standard FDB cluster file location, or to run against multiple clusters

If a file `fdb-environment.yaml` exists in the root of the working directory, it contains the configuration for the C API library
and a list of cluster files. Most tests will chose randomly from the provided cluster files when testing.

Here is an example file:
``` yaml
libraryPath: /Users/scott/fdb/bin/fdb-server-7.3.42-macos_arm64/lib
clusterFiles:
- /Users/scott/fdb/data/fdb-one.cluster
- /Users/scott/fdb/data/fdb-two.cluster
```



### deprecated properties file

[ this is being replaced by the yaml file described above, to better support multiple cluster files ]

If a file `fdb-environment.properties` exists in the root of the working directory, it contains environment variables that specify where to find
the local FDB. These settings will apply when running inside IntelliJ as well as from a command line and so are easier to manage than a shell script.
Expand Down
2 changes: 2 additions & 0 deletions fdb-extensions/fdb-extensions.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ dependencies {
compileOnly(libs.jsr305)

testImplementation(libs.bundles.test.impl)
testImplementation(libs.snakeyaml)
testRuntimeOnly(libs.bundles.test.runtime)
testCompileOnly(libs.bundles.test.compileOnly)
testAnnotationProcessor(libs.autoService)

testFixturesImplementation(libs.bundles.test.impl)
testFixturesImplementation(libs.snakeyaml)
testFixturesCompileOnly(libs.bundles.test.compileOnly)
testFixturesImplementation(libs.slf4j.api)
testFixturesAnnotationProcessor(libs.autoService)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* FDBTestEnvironment.java
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2015-2025 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.test;

import org.assertj.core.api.Assumptions;
import org.yaml.snakeyaml.Yaml;

import javax.annotation.Nullable;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;

/**
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about putting this in testFixtures, but since other projects already depend on the tests configuration, doing so would require moving all the other classes into a testFixtures, and I figured that would be better as a separate PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a change in main to move them into a test fixture, and then went to test publishing of testFixtures so that yaml-tests could be used by clients for their tests, but it looks like we can't publish testFixtures: e9db5c0#diff-9515853bb3a0c957c759e9c453a9e17941f4333751715e75d74efed681fadac0R60 I'm going to look into creating a test-utils library....

* Utility class for accessing {@code fdb-environment.yaml}, so that tests can know about fdb clusters
* that are available.
*/
public final class FDBTestEnvironment {
private static final List<String> clusterFiles;

static {
final String fdbEnvironment = System.getenv("FDB_ENVIRONMENT_YAML");

if (fdbEnvironment != null && !fdbEnvironment.isEmpty()) {
clusterFiles = List.copyOf(parseFDBEnvironmentYaml(fdbEnvironment));
} else {
clusterFiles = Collections.singletonList(null); // List.of does not allow null
}
}

@SuppressWarnings("unchecked")
private static List<String> parseFDBEnvironmentYaml(final String fdbEnvironment) {
Yaml yaml = new Yaml();
try (FileInputStream yamlInput = new FileInputStream(fdbEnvironment)) {
Object fdbConfig = yaml.load(yamlInput);
return (List<String>)((Map<?, ?>)fdbConfig).get("clusterFiles");
} catch (IOException e) {
throw new IllegalStateException("Could not read fdb-environment.yaml", e);
} catch (ClassCastException e) {
throw new IllegalStateException("Could not parse fdb environment file " + fdbEnvironment, e);
}
}

@Nullable
public static String getClusterFile(int i) {
return clusterFiles.get(i);
}

public static List<String> allClusterFiles() {
return clusterFiles;
}

public static String randomClusterFile() {
return clusterFiles.get(ThreadLocalRandom.current().nextInt(clusterFiles.size()));
}

/**
* Marks the current test as skipped if there are not the desired number of clusters available to test against.
* @param desiredCount the number of clusters to test against
*/
public static void assumeClusterCount(final int desiredCount) {
if (desiredCount > 1) {
Assumptions.assumeThat(clusterFiles.size()).as("Cluster file count").isGreaterThanOrEqualTo(desiredCount);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public void beforeAll(final ExtensionContext extensionContext) {
@Nonnull
public Database getDatabase() {
if (db == null) {
db = FDB.instance().open();
db = FDB.instance().open(FDBTestEnvironment.randomClusterFile());
}
return db;
}
Expand Down
1 change: 1 addition & 0 deletions fdb-record-layer-core/fdb-record-layer-core.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependencies {
testImplementation(libs.junit.platform)
testImplementation(libs.protobuf.util)
testCompileOnly(libs.spotbugs.annotations)
testImplementation(libs.snakeyaml)

testAnnotationProcessor(libs.autoService)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.apple.foundationdb.record.RecordCoreException;
import com.apple.foundationdb.record.TestHelpers;
import com.apple.foundationdb.record.test.FDBDatabaseExtension;
import com.apple.foundationdb.test.FDBTestEnvironment;
import com.apple.test.Tags;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -57,7 +58,7 @@ void testBlockingInAsyncException() {
// Make sure that we aren't holding on to previously created databases
factory.clear();

FDBDatabase database = factory.getDatabase();
FDBDatabase database = factory.getDatabase(FDBTestEnvironment.randomClusterFile());
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test could probably just use dbExtension.getDatabase(), but I didn't want to change too much in this PR. And you have to take care to make sure you're not reusing a database created in the setup.

assertEquals(BlockingInAsyncDetection.IGNORE_COMPLETE_EXCEPTION_BLOCKING, database.getBlockingInAsyncDetection());
assertThrows(BlockingInAsyncException.class, () -> callAsyncBlocking(database));
}
Expand All @@ -68,7 +69,7 @@ void testBlockingInAsyncWarning() {
factory.setBlockingInAsyncDetection(BlockingInAsyncDetection.IGNORE_COMPLETE_WARN_BLOCKING);
factory.clear();

FDBDatabase database = factory.getDatabase();
FDBDatabase database = factory.getDatabase(FDBTestEnvironment.randomClusterFile());
TestHelpers.assertLogs(FDBDatabase.class, FDBDatabase.BLOCKING_IN_ASYNC_CONTEXT_MESSAGE,
() -> {
callAsyncBlocking(database, true);
Expand All @@ -82,7 +83,7 @@ void testCompletedBlockingInAsyncWarning() {
factory.setBlockingInAsyncDetection(BlockingInAsyncDetection.WARN_COMPLETE_EXCEPTION_BLOCKING);
factory.clear();

FDBDatabase database = factory.getDatabase();
FDBDatabase database = factory.getDatabase(FDBTestEnvironment.randomClusterFile());
TestHelpers.assertLogs(FDBDatabase.class, FDBDatabase.BLOCKING_IN_ASYNC_CONTEXT_MESSAGE,
() -> database.asyncToSync(new FDBStoreTimer(), FDBStoreTimer.Waits.WAIT_ERROR_CHECK,
CompletableFuture.supplyAsync(() ->
Expand All @@ -95,7 +96,7 @@ void testBlockingCreatingAsyncDetection() {
factory.setBlockingInAsyncDetection(BlockingInAsyncDetection.WARN_COMPLETE_EXCEPTION_BLOCKING);
factory.clear();

FDBDatabase database = factory.getDatabase();
FDBDatabase database = factory.getDatabase(FDBTestEnvironment.randomClusterFile());
TestHelpers.assertLogs(FDBDatabase.class, FDBDatabase.BLOCKING_RETURNING_ASYNC_MESSAGE,
() -> returnAnAsync(database, MoreAsyncUtil.delayedFuture(200L, TimeUnit.MILLISECONDS, database.getScheduledExecutor())));
}
Expand All @@ -106,7 +107,7 @@ void testCompletedBlockingCreatingAsyncDetection() {
factory.setBlockingInAsyncDetection(BlockingInAsyncDetection.WARN_COMPLETE_EXCEPTION_BLOCKING);
factory.clear();

FDBDatabase database = factory.getDatabase();
FDBDatabase database = factory.getDatabase(FDBTestEnvironment.randomClusterFile());
TestHelpers.assertDidNotLog(FDBDatabase.class, FDBDatabase.BLOCKING_RETURNING_ASYNC_MESSAGE,
() -> returnAnAsync(database, CompletableFuture.completedFuture(10L)));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.apple.foundationdb.record.test.TestKeySpace;
import com.apple.foundationdb.record.test.TestKeySpacePathManagerExtension;
import com.apple.foundationdb.subspace.Subspace;
import com.apple.foundationdb.test.FDBTestEnvironment;
import com.apple.foundationdb.tuple.Tuple;
import com.apple.test.BooleanSource;
import com.apple.test.Tags;
Expand All @@ -54,6 +55,7 @@

import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
Expand All @@ -66,6 +68,7 @@
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
Expand Down Expand Up @@ -240,7 +243,7 @@ void testJoinNowOnNonCompletedFuture(BlockingInAsyncDetection behavior) {
if (behavior.throwExceptionOnBlocking()) {
assertThrows(BlockingInAsyncException.class, () -> database.joinNow(new CompletableFuture<>()));
} else {
FDBDatabase database2 = factory.getDatabase();
FDBDatabase database2 = factory.getDatabase(database.getClusterFile());
TestHelpers.assertLogs(FDBDatabase.class, FDBDatabase.BLOCKING_FOR_FUTURE_MESSAGE, () -> {
long val = database2.joinNow(MoreAsyncUtil.delayedFuture(100, TimeUnit.MILLISECONDS, database2.getScheduledExecutor())
.thenApply(vignore -> 1066L));
Expand Down Expand Up @@ -494,4 +497,31 @@ void cannotChangeAPIVersionAfterInit() {
assertEquals(initApiVersion, database.getAPIVersion());
}
}

@Test
void canAccessMultipleClusters() {
FDBTestEnvironment.assumeClusterCount(2);
final FDBDatabase database0 = dbExtension.getDatabase(0);
final FDBDatabase database1 = dbExtension.getDatabase(1);
final byte[] key = Tuple.from(UUID.randomUUID()).pack();
final byte[] value0 = Tuple.from("cluster0").pack();
final byte[] value1 = Tuple.from("cluster1").pack();
try (FDBRecordContext context0 = database0.openContext()) {
context0.ensureActive().set(key, value0);
context0.commit();
}
try (FDBRecordContext context1 = database1.openContext()) {
assertNull(context1.ensureActive().get(key).join());
context1.ensureActive().set(key, value1);
context1.commit();
}
try (FDBRecordContext context0 = database0.openContext()) {
assertArrayEquals(value0, context0.ensureActive().get(key).join());
context0.commit();
}
try (FDBRecordContext context1 = database1.openContext()) {
assertArrayEquals(value1, context1.ensureActive().get(key).join());
context1.commit();
}
}
}
Loading
Loading