diff --git a/fdb-record-layer-core/fdb-record-layer-core.gradle b/fdb-record-layer-core/fdb-record-layer-core.gradle
index 75708cffd9..7e5e6b5179 100644
--- a/fdb-record-layer-core/fdb-record-layer-core.gradle
+++ b/fdb-record-layer-core/fdb-record-layer-core.gradle
@@ -25,6 +25,13 @@ plugins {
apply from: rootProject.file('gradle/proto.gradle')
apply from: rootProject.file('gradle/publishing.gradle')
+configurations {
+ // Resolved to the mockito-core jar so it can be installed as a -javaagent on the test JVM.
+ // Required on JDK 21+ because Mockito's inline mock maker can no longer self-attach.
+ // See https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3
+ mockitoAgent
+}
+
dependencies {
annotationProcessor project(':fdb-java-annotations')
api project(':fdb-extensions')
@@ -60,6 +67,12 @@ dependencies {
testFixturesImplementation(libs.protobuf.util)
testFixturesCompileOnly(libs.jsr305)
testFixturesAnnotationProcessor(libs.autoService)
+
+ mockitoAgent(libs.mockito) { transitive = false }
+}
+
+tasks.withType(Test).configureEach {
+ jvmArgs("-javaagent:${configurations.mockitoAgent.asPath}")
}
sourceSets {
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/FDBRecordStore.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/FDBRecordStore.java
index 2a135af001..7f8d414db4 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/FDBRecordStore.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/FDBRecordStore.java
@@ -1799,12 +1799,16 @@ public static void deleteStore(FDBRecordContext context, KeySpacePath path) {
* the store existed.
*
*
- * This method does not read the underlying record store, so it does not validate
- * that a record store exists in the given subspace. As it might be the case that
- * this record store has a cacheable store state (see {@link #setStateCacheability(boolean)}),
- * this method resets the database's
- * {@linkplain FDBRecordContext#getMetaDataVersionStamp(IsolationLevel) meta-data version-stamp}.
- * As a result, calling this method may cause other clients to invalidate their caches needlessly.
+ * This method reads only the record store's header key (see {@link #STORE_INFO_KEY}) in
+ * order to decide whether it needs to invalidate cached state. If a header is present and
+ * marks the store as {@linkplain #setStateCacheability(boolean) cacheable}, the database's
+ * {@linkplain FDBRecordContext#getMetaDataVersionStamp(IsolationLevel) meta-data
+ * version-stamp} is reset so that other clients drop their cached copies; if the header is
+ * missing (store doesn't exist) or marks the store as non-cacheable, the version-stamp is
+ * not touched. This means callers who only ever operate on non-cacheable stores do not
+ * contend on the single meta-data version-stamp key. The header read uses snapshot
+ * isolation and does not add a read conflict, so it does not narrow the write-conflict
+ * behavior of the clear that follows.
*
*
* @param context the transactional context in which to delete the record store
@@ -1812,9 +1816,25 @@ public static void deleteStore(FDBRecordContext context, KeySpacePath path) {
*/
@SuppressWarnings("PMD.CloseResource")
public static void deleteStore(FDBRecordContext context, Subspace subspace) {
- // In theory, we only need to set the meta-data version stamp if the record store's
- // meta-data is cacheable, but we can't know that from here.
- context.setMetaDataVersionStamp();
+ // Read the store header at snapshot isolation so we don't add a needless read conflict:
+ // the clear below already creates a write conflict on the whole range, which is what
+ // actually serialises us against any concurrent writer of this store's header.
+ final byte[] headerKey = subspace.pack(STORE_INFO_KEY);
+ final byte[] headerBytes = context.asyncToSync(FDBStoreTimer.Waits.WAIT_LOAD_RECORD_STORE_STATE,
+ context.readTransaction(true).get(headerKey));
+ if (headerBytes != null) {
+ boolean shouldBump = true;
+ try {
+ shouldBump = RecordMetaDataProto.DataStoreInfo.parseFrom(headerBytes).getCacheable();
+ } catch (InvalidProtocolBufferException e) {
+ // If we can't parse the header, fall back to the pre-change conservative
+ // behavior and bump. This preserves the "delete a store I know nothing about"
+ // contract this method advertises.
+ }
+ if (shouldBump) {
+ context.setMetaDataVersionStamp();
+ }
+ }
context.setDirtyStoreState(true);
context.clear(subspace.range());
}
@@ -3019,9 +3039,34 @@ private CompletableFuture updateStoreHeaderAsync(@Nonnull UnaryOperator readStoreFirstKey(@Nonnull FDBRecordContext context, @Nonnull Subspace subspace, @Nonnull IsolationLevel isolationLevel) {
- final AsyncIterator iterator = context.readTransaction(isolationLevel.isSnapshot()).getRange(subspace.range(), 1).iterator();
+ // Always read at snapshot isolation so FDB does not add an implicit read-conflict range
+ // spanning the whole subspace (which can pull in unrelated concurrent writers deep inside
+ // the store's subspace and produce spurious SERIALIZATION_FAILUREs). When the caller
+ // wanted SERIALIZABLE isolation, add back a narrower, deterministic read-conflict range
+ // ourselves: from the beginning of the subspace up to and including the first key we
+ // actually found. When the store is empty (no keys at all) we still have to cover the
+ // whole range to correctly serialise against a concurrent creator, so we widen back to
+ // the full subspace in that case.
+ final Range subspaceRange = subspace.range();
+ final AsyncIterator iterator = context.readTransaction(true).getRange(subspaceRange, 1).iterator();
return context.instrument(FDBStoreTimer.Events.LOAD_RECORD_STORE_INFO,
- iterator.onHasNext().thenApply(hasNext -> hasNext ? iterator.next() : null));
+ iterator.onHasNext().thenApply(hasNext -> {
+ final KeyValue firstKey = hasNext ? iterator.next() : null;
+ if (!isolationLevel.isSnapshot()) {
+ final byte[] end;
+ if (firstKey == null) {
+ // Store is empty — we need to serialise against anyone else who might
+ // write into this subspace between our read and commit.
+ end = subspaceRange.end;
+ } else {
+ // Cover [subspaceRange.begin, firstKey + \x00) — i.e. the found key
+ // itself and everything before it, but nothing after.
+ end = ByteArrayUtil.join(firstKey.getKey(), new byte[]{0});
+ }
+ context.ensureActive().addReadConflictRange(subspaceRange.begin, end);
+ }
+ return firstKey;
+ }));
}
/**
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/InstrumentedTransaction.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/InstrumentedTransaction.java
index 942b749962..3745a2e6f5 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/InstrumentedTransaction.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/InstrumentedTransaction.java
@@ -30,6 +30,10 @@
import com.apple.foundationdb.annotation.API;
import com.apple.foundationdb.async.AsyncUtil;
import com.apple.foundationdb.record.provider.common.StoreTimer;
+import com.apple.foundationdb.tuple.ByteArrayUtil;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -39,10 +43,21 @@
/**
* Wrapper around {@link Transaction} that instruments certain calls to expose their behavior with
* {@link FDBStoreTimer} metrics.
+ *
+ *
When this class's logger is set to {@code TRACE}, every mutation call
+ * ({@link #clear(byte[])}, {@link #clear(byte[], byte[])}, {@link #clear(Range)},
+ * {@link #clearRangeStartsWith(byte[])}, and range-based
+ * {@link #addWriteConflictRange(byte[], byte[])}) emits the affected key range along with a
+ * captured stack trace. This is intended purely for diagnostic use — for example when hunting
+ * down which code path is issuing a wide subspace clear that's causing a
+ * {@code SERIALIZATION_FAILURE} on a concurrent read. Leaving the logger at its default level
+ * (WARN or above) keeps the overhead a single {@code isTraceEnabled()} check per call.
*/
@API(API.Status.INTERNAL)
public class InstrumentedTransaction extends InstrumentedReadTransaction implements Transaction {
+ private static final Logger LOGGER = LoggerFactory.getLogger(InstrumentedTransaction.class);
+
@Nullable
protected ReadTransaction snapshot; // lazily cached snapshot wrapper
@@ -77,6 +92,7 @@ public void addReadConflictKey(byte[] key) {
@Override
public void addWriteConflictRange(byte[] keyBegin, byte[] keyEnd) {
+ traceMutation("addWriteConflictRange", keyBegin, keyEnd);
underlying.addWriteConflictRange(checkKey(keyBegin), checkKey(keyEnd));
}
@@ -94,12 +110,14 @@ public void set(byte[] key, byte[] value) {
@Override
public void clear(byte[] key) {
+ traceMutation("clear(key)", key, null);
underlying.clear(checkKey(key));
increment(FDBStoreTimer.Counts.DELETES);
}
@Override
public void clear(byte[] keyBegin, byte[] keyEnd) {
+ traceMutation("clear(range)", keyBegin, keyEnd);
underlying.clear(checkKey(keyBegin), checkKey(keyEnd));
increment(FDBStoreTimer.Counts.DELETES);
increment(FDBStoreTimer.Counts.RANGE_DELETES);
@@ -107,6 +125,7 @@ public void clear(byte[] keyBegin, byte[] keyEnd) {
@Override
public void clear(Range range) {
+ traceMutation("clear(Range)", range.begin, range.end);
checkKey(range.begin);
checkKey(range.end);
@@ -118,11 +137,28 @@ public void clear(Range range) {
@Override
@Deprecated
public void clearRangeStartsWith(byte[] prefix) {
+ traceMutation("clearRangeStartsWith", prefix, null);
underlying.clearRangeStartsWith(checkKey(prefix));
increment(FDBStoreTimer.Counts.DELETES);
increment(FDBStoreTimer.Counts.RANGE_DELETES);
}
+ /**
+ * Diagnostic hook: when this class's SLF4J logger is at TRACE, emit the operation name, the
+ * printable form of the affected key(s), and a stack trace. Callers pay only the cost of the
+ * {@code isTraceEnabled()} check at other log levels. Intended for hunting down subspace-wide
+ * range writes that cause serialization failures on concurrent readers.
+ */
+ private static void traceMutation(@Nonnull String op, @Nullable byte[] begin, @Nullable byte[] end) {
+ if (!LOGGER.isTraceEnabled()) {
+ return;
+ }
+ final String beginStr = begin == null ? "null" : ByteArrayUtil.printable(begin);
+ final String endStr = end == null ? "" : ".." + ByteArrayUtil.printable(end);
+ LOGGER.trace("InstrumentedTransaction mutation: {} [{}{}]", op, beginStr, endStr,
+ new Throwable("stack for " + op));
+ }
+
@Override
public void mutate(MutationType opType, byte[] key, byte[] param) {
underlying.mutate(opType, checkKey(key), param);
diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/FDBDatabaseTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/FDBDatabaseTest.java
index 2e0a108c04..fd7ebe80ae 100644
--- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/FDBDatabaseTest.java
+++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/FDBDatabaseTest.java
@@ -107,12 +107,19 @@ void cachedVersionMaintenanceOnReadsTest() throws Exception {
RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor());
+ // Create the path up-front so that the directory-layer resolution done by
+ // TestKeySpacePathManager.createPath has already warmed FDBDatabase.lastSeenFDBVersion
+ // by the time we capture readVersion1 below. Otherwise the cached version captured
+ // here would be stale relative to the one createPath puts in the cache, and the
+ // weak-read-semantics assertion at "assertEquals(readVersion1, context.getReadVersion())"
+ // would return the newer (post-createPath) cached version and fail.
+ final KeySpacePath path = pathManager.createPath(TestKeySpace.RECORD_STORE);
+
// First time, does a GRV from FDB
- long readVersion1 = getReadVersion(database, 0L, 2_000L);
+ long readVersion1 = getReadVersion(database, 0L, 60_000L);
// Store a record (advances future GRV, but not cached version)
- final KeySpacePath path = pathManager.createPath(TestKeySpace.RECORD_STORE);
- final FDBDatabase.WeakReadSemantics weakReadSemantics = new FDBDatabase.WeakReadSemantics(0L, 2_000L, false);
+ final FDBDatabase.WeakReadSemantics weakReadSemantics = new FDBDatabase.WeakReadSemantics(0L, 60_000L, false);
try (FDBRecordContext context = database.openContext(FDBRecordContextConfig.newBuilder().setWeakReadSemantics(weakReadSemantics).setMdcContext(MDC.getCopyOfContextMap()).build())) {
assertEquals(readVersion1, context.getReadVersion());
FDBRecordStore recordStore = FDBRecordStore.newBuilder()
@@ -126,8 +133,8 @@ void cachedVersionMaintenanceOnReadsTest() throws Exception {
context.commit();
}
- // We're fine with any version obtained up to 2s ago, so will get the original readVersion
- assertEquals(readVersion1, getReadVersion(database, 0L, 2000L));
+ // We're fine with any version obtained up to 60s ago, so will get the original readVersion
+ assertEquals(readVersion1, getReadVersion(database, 0L, 60_000L));
Thread.sleep(10L);
@@ -135,19 +142,32 @@ void cachedVersionMaintenanceOnReadsTest() throws Exception {
long readVersion2 = getReadVersion(database, 0L, 11L);
assertThat(readVersion1, lessThan(readVersion2));
- // Store another record
- testStoreAndRetrieveSimpleRecord(database, metaData, path);
+ // Store another record. Disable read-tracking for the duration so the fresh GRV
+ // done inside testStoreAndRetrieveSimpleRecord (which uses database.run(null, ...)
+ // and therefore bypasses weak-read-semantics) doesn't repopulate the cached
+ // version with a newer value than readVersion2 and break the assertions below.
+ database.setTrackLastSeenVersionOnRead(false);
+ try {
+ testStoreAndRetrieveSimpleRecord(database, metaData, path);
+ } finally {
+ database.setTrackLastSeenVersionOnRead(true);
+ }
- assertEquals(readVersion2, getReadVersion(database, 0L, 2000L));
- assertEquals(readVersion2, getReadVersion(database, readVersion2, 2000L));
+ assertEquals(readVersion2, getReadVersion(database, 0L, 60_000L));
+ assertEquals(readVersion2, getReadVersion(database, readVersion2, 60_000L));
// Now we want at least readVersion2 + 1, so this will cause a GRV
- long readVersion3 = getReadVersion(database, readVersion2 + 1, 2000L);
+ long readVersion3 = getReadVersion(database, readVersion2 + 1, 60_000L);
assertTrue(readVersion2 < readVersion3);
- // Store another record
- testStoreAndRetrieveSimpleRecord(database, metaData, path);
+ // Store another record (same read-tracking-disable shield as above)
+ database.setTrackLastSeenVersionOnRead(false);
+ try {
+ testStoreAndRetrieveSimpleRecord(database, metaData, path);
+ } finally {
+ database.setTrackLastSeenVersionOnRead(true);
+ }
// Don't use a stored version
assertTrue(readVersion3 < getReadVersion(database, null, null));
diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/FDBRecordStoreReplaceIndexTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/FDBRecordStoreReplaceIndexTest.java
index 2b76e6e1bb..78f30992bb 100644
--- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/FDBRecordStoreReplaceIndexTest.java
+++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/FDBRecordStoreReplaceIndexTest.java
@@ -47,7 +47,6 @@
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
-import java.util.stream.IntStream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
@@ -235,20 +234,20 @@ public void buildReplacementsInMultipleStores() {
final Index newIndex = new Index("MySimpleRecord$(num_value_2, repeater)",
Key.Expressions.concat(Key.Expressions.field("num_value_2"), Key.Expressions.field("repeater", KeyExpression.FanType.FanOut)));
final RecordMetaDataHook allIndexesHook = composeHooks(addIndexHook(recordTypeName, origIndex), addIndexHook(recordTypeName, newIndex));
- final List stores = IntStream.range(0, 10).mapToObj(i -> "store_" + i).collect(Collectors.toList());
+ final int storeCount = 10;
try (FDBRecordContext context = openContext()) {
openSimpleRecordStore(context, allIndexesHook);
// Create a bunch of stores and disable the new index in all of them
- forEachStore(multiStoreRoot, stores, (storePathName, subStore) -> {
+ forEachStore(multiStoreRoot, storeCount, (index, subStore) -> {
assertTrue(context.asyncToSync(FDBStoreTimer.Waits.WAIT_DROP_INDEX, subStore.markIndexDisabled(newIndex)));
subStore.saveRecord(TestRecords1Proto.MySimpleRecord.newBuilder()
.setRecNo(1066L)
.addRepeater(42)
.addRepeater(800)
- .setStrValueIndexed(storePathName)
+ .setStrValueIndexed("String " + index)
.build());
});
@@ -260,7 +259,7 @@ public void buildReplacementsInMultipleStores() {
try (FDBRecordContext context = openContext()) {
openSimpleRecordStore(context, withReplacementHook);
- forEachStore(multiStoreRoot, stores, (storePathName, subStore) -> {
+ forEachStore(multiStoreRoot, storeCount, (index, subStore) -> {
final List> records = context.asyncToSync(FDBStoreTimer.Waits.WAIT_SCAN_RECORDS, subStore.scanIndexRecords(origIndex.getName()).asList());
assertThat(records, hasSize(2));
records.stream()
@@ -270,7 +269,7 @@ public void buildReplacementsInMultipleStores() {
assertThat(fieldValue, instanceOf(String.class));
return (String)fieldValue;
})
- .forEach(strValue -> assertEquals(storePathName, strValue));
+ .forEach(strValue -> assertEquals("String " + index, strValue));
context.asyncToSync(FDBStoreTimer.Waits.WAIT_ONLINE_BUILD_INDEX, subStore.rebuildIndex(subStore.getRecordMetaData().getIndex(newIndex.getName())));
});
@@ -281,14 +280,14 @@ public void buildReplacementsInMultipleStores() {
// Validate that each store has had the original index removed (because the replacement index was built)
try (FDBRecordContext context = openContext()) {
openSimpleRecordStore(context, withReplacementHook);
- forEachStore(multiStoreRoot, stores, (storePathName, subStore) -> assertTrue(subStore.isIndexDisabled(origIndex.getName())));
+ forEachStore(multiStoreRoot, storeCount, (storePathName, subStore) -> assertTrue(subStore.isIndexDisabled(origIndex.getName())));
commit(context);
}
// Validate each store has had the old index data cleaned out
try (FDBRecordContext context = openContext()) {
openSimpleRecordStore(context, composeHooks(allIndexesHook, bumpMetaDataVersionHook(), bumpMetaDataVersionHook()));
- forEachStore(multiStoreRoot, stores, (storePathName, subStore) -> {
+ forEachStore(multiStoreRoot, storeCount, (storePathName, subStore) -> {
assertTrue(context.asyncToSync(FDBStoreTimer.Waits.WAIT_ADD_INDEX, subStore.uncheckedMarkIndexReadable(origIndex.getName())));
assertEquals(Collections.emptyList(), context.asyncToSync(FDBStoreTimer.Waits.WAIT_SCAN_INDEX_RECORDS, subStore.scanIndexRecords(origIndex.getName()).asList()));
});
@@ -302,13 +301,13 @@ public void buildReplacementsInMultipleStores() {
}
}
- private void forEachStore(@Nonnull KeySpacePath root, @Nonnull List storePaths, @Nonnull BiConsumer subStoreConsumer) {
- for (String storePathName : storePaths) {
- final KeySpacePath storePath = root.add(TestKeySpace.STORE_PATH, storePathName);
+ private void forEachStore(@Nonnull KeySpacePath root, int count, @Nonnull BiConsumer subStoreConsumer) {
+ for (int i = 0; i < count; i++) {
+ final KeySpacePath storePath = root.add(TestKeySpace.STORE_PATH, (long) i);
final FDBRecordStore subStore = recordStore.asBuilder()
.setKeySpacePath(storePath)
.createOrOpen();
- subStoreConsumer.accept(storePathName, subStore);
+ subStoreConsumer.accept(i, subStore);
}
}
diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/FDBReverseDirectoryCacheTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/FDBReverseDirectoryCacheTest.java
index 5f8e48964a..887a7c2fdc 100644
--- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/FDBReverseDirectoryCacheTest.java
+++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/FDBReverseDirectoryCacheTest.java
@@ -34,8 +34,8 @@
import com.apple.foundationdb.record.test.FDBDatabaseExtension;
import com.apple.foundationdb.record.test.TestKeySpace;
import com.apple.foundationdb.record.test.TestKeySpacePathManagerExtension;
-import com.apple.foundationdb.test.FDBTestEnvironment;
import com.apple.foundationdb.record.util.pair.Pair;
+import com.apple.foundationdb.test.FDBTestEnvironment;
import com.apple.foundationdb.tuple.Tuple;
import com.apple.test.Tags;
import com.google.common.collect.BiMap;
@@ -59,6 +59,7 @@
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
@@ -658,12 +659,40 @@ private Pair[] createRandomDirectoryEntries(FDBDatabase database,
keys.add("dir_" + Math.abs(random.nextInt()));
}
List values = keys.stream()
- .map(key -> globalScope.resolve(context.getTimer(), key).join())
+ .map(key -> resolveWithRetry(key, context.getTimer()))
.collect(Collectors.toList());
return zipKeysAndValues(keys, values);
}
}
+ /**
+ * Resolve a name in {@link #globalScope} with a retry loop around the specific
+ * "database already has keys in allocation range" / "database has keys stored at the prefix
+ * chosen by the automatic prefix allocator" failures that the root
+ * {@link com.apple.foundationdb.directory.DirectoryLayer.HighContentionAllocator}
+ * throws when a randomly-chosen candidate byte prefix happens to already have keys under it.
+ * Under parallel test execution any sibling test committing writes at low-integer prefixes
+ * can trip this check on a fresh candidate — the allocation is fine to retry, since the HCA
+ * picks a new random candidate each attempt.
+ */
+ private Long resolveWithRetry(String key, FDBStoreTimer timer) {
+ final int maxAttempts = 20;
+ RuntimeException lastFailure = null;
+ for (int attempt = 0; attempt < maxAttempts; attempt++) {
+ try {
+ return globalScope.resolve(timer, key).join();
+ } catch (CompletionException | IllegalStateException e) {
+ Throwable cause = e instanceof CompletionException ? e.getCause() : e;
+ if (cause instanceof IllegalStateException) {
+ lastFailure = e;
+ continue;
+ }
+ throw e;
+ }
+ }
+ throw new AssertionError("globalScope.resolve failed after " + maxAttempts + " attempts", lastFailure);
+ }
+
@SuppressWarnings({"rawtypes", "unchecked"})
private static Pair[] zipKeysAndValues(List keys, List values) {
final Iterator valuesIter = values.iterator();
diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexScrubberTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexScrubberTest.java
index 644f21f68f..475217a591 100644
--- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexScrubberTest.java
+++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexScrubberTest.java
@@ -40,6 +40,7 @@
import com.apple.foundationdb.tuple.Tuple;
import com.google.protobuf.Message;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
@@ -63,6 +64,12 @@
/**
* Tests for scrubbing readable indexes with {@link OnlineIndexer}.
*/
+// Scrubber tests issue many batch-priority GRVs per method and still trip the
+// FDB cluster's batch-GRV rate limit even when serialised with other indexer
+// tests via the inherited @ResourceLock from OnlineIndexerTest. @Isolated
+// escalates to full cross-class isolation so no other tests compete for FDB
+// resources while the scrubber runs.
+@Isolated("Scrubber tests inherently issue many batch-priority GRVs and trip FDB's batch-GRV rate limit when sharing the cluster with other parallel tests")
class OnlineIndexScrubberTest extends OnlineIndexerTest {
private FDBRecordStoreTestBase.RecordMetaDataHook myHook(Index index) {
diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerBuildGroupedCountIndexTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerBuildGroupedCountIndexTest.java
index 4afb2f5f5d..93f28394c4 100644
--- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerBuildGroupedCountIndexTest.java
+++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerBuildGroupedCountIndexTest.java
@@ -36,6 +36,7 @@
import com.google.common.collect.Maps;
import com.google.protobuf.Message;
import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.parallel.Isolated;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@@ -61,6 +62,11 @@
/**
* Tests for building a grouped count index.
*/
+// Parameterized invocations of buildWhileUpdatingGroupedCount issue many batch-priority
+// GRVs and trip the FDB cluster's batch-GRV rate limit even when the class is serialised
+// with other indexer tests via the inherited @ResourceLock from OnlineIndexerTest.
+// Escalate to full cross-class isolation, matching OnlineIndexScrubberTest.
+@Isolated("Grouped-count indexer tests issue many batch-priority GRVs and trip FDB's batch-GRV rate limit when sharing the cluster with other parallel tests")
public class OnlineIndexerBuildGroupedCountIndexTest extends OnlineIndexerTest {
private static final KeyExpression PRIMARY_KEY = concatenateFields("num_value_2", "rec_no");
diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerMergeTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerMergeTest.java
index 188c030fe2..adc193b768 100644
--- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerMergeTest.java
+++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerMergeTest.java
@@ -53,6 +53,8 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.ResourceAccessMode;
+import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@@ -80,6 +82,9 @@
* Tests specifically of {@link OnlineIndexer#mergeIndex()}.
*/
@Tag(Tags.RequiresFDB)
+// Matches OnlineIndexerTest's @ResourceLock so this class serialises with the rest of the
+// OnlineIndexer* test classes, avoiding FDB batch-GRV throttle when run in parallel.
+@ResourceLock(value = "ONLINE_INDEXER_BATCH_GRV", mode = ResourceAccessMode.READ_WRITE)
public class OnlineIndexerMergeTest extends FDBRecordStoreConcurrentTestBase {
private static final String INDEX_NAME = "mergableIndex";
diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerTest.java
index c7a0ed0ef8..faf75ce645 100644
--- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerTest.java
+++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerTest.java
@@ -43,6 +43,8 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.parallel.ResourceAccessMode;
+import org.junit.jupiter.api.parallel.ResourceLock;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -62,6 +64,13 @@
* Tests for {@link OnlineIndexer}.
*/
@Tag(Tags.RequiresFDB)
+// Online indexer operations use batch-priority GRV (OnlineIndexOperationBaseBuilder
+// defaults to FDBTransactionPriority.BATCH). When many indexer test classes run in
+// parallel they overrun the FDB cluster's batch-GRV rate limit, causing flaky
+// "Batch GRV request rate limit exceeded" failures. This @ResourceLock is inherited
+// by all 15 subclasses of OnlineIndexerTest, serialising indexer tests with each
+// other while still allowing them to run in parallel with non-indexer tests.
+@ResourceLock(value = "ONLINE_INDEXER_BATCH_GRV", mode = ResourceAccessMode.READ_WRITE)
public abstract class OnlineIndexerTest {
@RegisterExtension
final FDBDatabaseExtension dbExtension = new FDBDatabaseExtension();
diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/SynchronizedSessionTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/SynchronizedSessionTest.java
index ee42087d61..167afefc6a 100644
--- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/SynchronizedSessionTest.java
+++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/SynchronizedSessionTest.java
@@ -32,6 +32,7 @@
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.parallel.Isolated;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -352,6 +353,7 @@ private void waitLongEnough() throws InterruptedException {
/**
* Run {@link SynchronizedSessionRunner} synchronously.
*/
+ @Isolated("Lease-renewal tests Thread.sleep then assert the lease was renewed; under parallel CPU starvation the renewing thread misses its window and the assertion fails")
public static class Run extends SynchronizedSessionTest {
Run() {
super(false);
@@ -361,6 +363,7 @@ public static class Run extends SynchronizedSessionTest {
/**
* Run {@link SynchronizedSessionRunner} asynchronously.
*/
+ @Isolated("Lease-renewal tests Thread.sleep then assert the lease was renewed; under parallel CPU starvation the renewing thread misses its window and the assertion fails")
public static class RunAsync extends SynchronizedSessionTest {
RunAsync() {
super(true);
diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/indexes/VectorIndexIndexingTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/indexes/VectorIndexIndexingTest.java
index a57a861403..837ff78b0d 100644
--- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/indexes/VectorIndexIndexingTest.java
+++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/indexes/VectorIndexIndexingTest.java
@@ -34,6 +34,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.protobuf.Message;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
import javax.annotation.Nonnull;
import java.util.List;
@@ -49,6 +50,8 @@
* Tests online indexing of vector indexes.
* These tests verify that vector indexes can be built asynchronously after records are saved.
*/
+// TODO maybe change this to the same resource lock as the indexing tests
+@Isolated("Heavy vector index-builder trips FDB cluster's batch-priority GRV rate limit when run concurrently with other indexer tests")
class VectorIndexIndexingTest extends VectorIndexTestBase {
@Test
diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/indexes/VectorIndexTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/indexes/VectorIndexTest.java
index 922109400f..2eb0436799 100644
--- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/indexes/VectorIndexTest.java
+++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/indexes/VectorIndexTest.java
@@ -276,7 +276,7 @@ private void checkResultsGrouped(@Nonnull final RecordQueryIndexPlan indexPlan,
void insertReadDeleteReadGroupedWithContinuationTest(final long seed, final boolean useAsync, final int limit) throws Exception {
final int size = 1000;
Assertions.assertThat(size % 2).isEqualTo(0); // needs to be even
- final int updateBatchSize = 50;
+ final int updateBatchSize = 25;
Assertions.assertThat(size % updateBatchSize).isEqualTo(0); // needs to be divisible
final int k = 100;
diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/indexes/VersionIndexTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/indexes/VersionIndexTest.java
index b0227434ed..1db8aa2d54 100644
--- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/indexes/VersionIndexTest.java
+++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/indexes/VersionIndexTest.java
@@ -3045,9 +3045,11 @@ void deleteStoreWithUncommittedVersionData(FormatVersion testFormatVersion, bool
splitLongRecords = testSplitLongRecords;
final KeySpacePath baseMultiPath = pathManager.createPath(TestKeySpace.MULTI_RECORD_STORE);
- final KeySpacePath path1 = baseMultiPath.add(TestKeySpace.STORE_PATH, "path1");
- final KeySpacePath path2 = baseMultiPath.add(TestKeySpace.STORE_PATH, "path2");
- final KeySpacePath path3 = baseMultiPath.add(TestKeySpace.STORE_PATH, "path3");
+ // STORE_PATH is a KeyType.LONG KeySpaceDirectory (see TestKeySpace) — pass numeric ids
+ // rather than strings so no shared root-DL allocation happens here.
+ final KeySpacePath path1 = baseMultiPath.add(TestKeySpace.STORE_PATH, 1L);
+ final KeySpacePath path2 = baseMultiPath.add(TestKeySpace.STORE_PATH, 2L);
+ final KeySpacePath path3 = baseMultiPath.add(TestKeySpace.STORE_PATH, 3L);
final List multiPaths = List.of(path1, path2, path3);
final Map version1ByStore = new HashMap<>();
diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/storestate/FDBRecordStoreStateCacheTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/storestate/FDBRecordStoreStateCacheTest.java
index 2e866a2a47..13f8cc17a5 100644
--- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/storestate/FDBRecordStoreStateCacheTest.java
+++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/storestate/FDBRecordStoreStateCacheTest.java
@@ -45,6 +45,7 @@
import com.apple.foundationdb.record.provider.foundationdb.RecordStoreStaleMetaDataVersionException;
import com.apple.foundationdb.record.provider.foundationdb.keyspace.KeySpacePath;
import com.apple.foundationdb.record.test.FakeClusterFileUtil;
+import com.apple.foundationdb.subspace.Subspace;
import com.apple.foundationdb.tuple.ByteArrayUtil;
import com.apple.foundationdb.tuple.ByteArrayUtil2;
import com.apple.foundationdb.tuple.Tuple;
@@ -766,6 +767,314 @@ public void storeDeletionAcrossContexts(@Nonnull StateCacheTestContext testConte
}
}
+ /**
+ * Deleting a non-cacheable store must NOT bump the meta-data version stamp: no other
+ * client can possibly hold a cached copy of a non-cacheable header, so the version stamp
+ * (a JVM-wide bottleneck on the {@code \xff/metadataVersion} key) shouldn't be touched.
+ * This is the low-level property that lets parallel tests share the SYS catalog without
+ * conflicting on catalog teardown.
+ */
+ @Test
+ void deleteNonCacheableStoreDoesNotBumpMetaDataVersionStamp() throws Exception {
+ // Bootstrap: ensure the meta-data version stamp key has a value before the test runs,
+ // so we can distinguish "unchanged" from "was never set". The store below is opened
+ // with cacheability disabled (which is the default from setStateCacheability(false)).
+ try (FDBRecordContext context = fdb.openContext()) {
+ if (context.getMetaDataVersionStamp(IsolationLevel.SNAPSHOT) == null) {
+ context.setMetaDataVersionStamp();
+ }
+ commit(context);
+ }
+
+ Subspace subspace;
+ try (FDBRecordContext context = openContext()) {
+ openSimpleRecordStore(context);
+ assertFalse(recordStore.getRecordStoreState().getStoreHeader().getCacheable(),
+ "test presumes the store is non-cacheable by default");
+ subspace = recordStore.getSubspace();
+ commit(context);
+ }
+
+ // Snapshot the version stamp before deletion — reading via SNAPSHOT so this txn doesn't
+ // conflict with the delete-txn below and skew the result.
+ final byte[] beforeStamp;
+ try (FDBRecordContext context = fdb.openContext()) {
+ beforeStamp = context.getMetaDataVersionStamp(IsolationLevel.SNAPSHOT);
+ }
+ assertNotNull(beforeStamp, "bootstrap should have populated the meta-data version stamp");
+
+ try (FDBRecordContext context = openContext()) {
+ FDBRecordStore.deleteStore(context, subspace);
+ commit(context);
+ }
+
+ try (FDBRecordContext context = fdb.openContext()) {
+ final byte[] afterStamp = context.getMetaDataVersionStamp(IsolationLevel.SNAPSHOT);
+ assertArrayEquals(beforeStamp, afterStamp,
+ "deleting a non-cacheable store should not have bumped the meta-data version stamp");
+ }
+ }
+
+ /**
+ * Complement of {@link #deleteNonCacheableStoreDoesNotBumpMetaDataVersionStamp()}: deleting
+ * a cacheable store MUST bump the stamp — otherwise sibling clients could keep serving
+ * reads out of a stale cached header long after the store is gone.
+ */
+ @Test
+ void deleteCacheableStoreBumpsMetaDataVersionStamp() throws Exception {
+ try (FDBRecordContext context = fdb.openContext()) {
+ if (context.getMetaDataVersionStamp(IsolationLevel.SNAPSHOT) == null) {
+ context.setMetaDataVersionStamp();
+ }
+ commit(context);
+ }
+
+ Subspace subspace;
+ try (FDBRecordContext context = openContext()) {
+ openSimpleRecordStore(context);
+ assertTrue(recordStore.setStateCacheability(true), "flipping to cacheable should have changed something");
+ subspace = recordStore.getSubspace();
+ commit(context);
+ }
+ // Commit above already bumped the stamp (transition to cacheable). Snapshot after that.
+ final byte[] beforeStamp;
+ try (FDBRecordContext context = fdb.openContext()) {
+ beforeStamp = context.getMetaDataVersionStamp(IsolationLevel.SNAPSHOT);
+ }
+ assertNotNull(beforeStamp);
+
+ try (FDBRecordContext context = openContext()) {
+ FDBRecordStore.deleteStore(context, subspace);
+ commit(context);
+ }
+
+ try (FDBRecordContext context = fdb.openContext()) {
+ final byte[] afterStamp = context.getMetaDataVersionStamp(IsolationLevel.SNAPSHOT);
+ assertNotNull(afterStamp);
+ assertFalse(java.util.Arrays.equals(beforeStamp, afterStamp),
+ "deleting a cacheable store should have bumped the meta-data version stamp");
+ }
+ }
+
+ /**
+ * Deleting an empty subspace (no store header present) must not bump the stamp either —
+ * there's no cached header to invalidate.
+ */
+ @Test
+ void deleteMissingStoreDoesNotBumpMetaDataVersionStamp() throws Exception {
+ try (FDBRecordContext context = fdb.openContext()) {
+ if (context.getMetaDataVersionStamp(IsolationLevel.SNAPSHOT) == null) {
+ context.setMetaDataVersionStamp();
+ }
+ commit(context);
+ }
+
+ // Use the test's per-instance path, but never open a store there.
+ final Subspace subspace;
+ try (FDBRecordContext context = openContext()) {
+ subspace = path.toSubspace(context);
+ }
+ final byte[] beforeStamp;
+ try (FDBRecordContext context = fdb.openContext()) {
+ beforeStamp = context.getMetaDataVersionStamp(IsolationLevel.SNAPSHOT);
+ }
+ assertNotNull(beforeStamp);
+
+ try (FDBRecordContext context = openContext()) {
+ FDBRecordStore.deleteStore(context, subspace);
+ commit(context);
+ }
+
+ try (FDBRecordContext context = fdb.openContext()) {
+ final byte[] afterStamp = context.getMetaDataVersionStamp(IsolationLevel.SNAPSHOT);
+ assertArrayEquals(beforeStamp, afterStamp,
+ "deleting an empty subspace (no header) should not have bumped the meta-data version stamp");
+ }
+ }
+
+ /**
+ * Regression test that pins the current behaviour: on a state-cache miss, opening a
+ * cacheable store issues a {@code SERIALIZABLE getRange(subspace.range(), 1)} inside
+ * {@code FDBRecordStore.loadStoreHeaderAsync}, which registers a read-conflict range that
+ * covers (at minimum) the store header key. Any concurrent transaction that writes to the
+ * store header — including the very common case of a second opener that itself hits a cache
+ * miss and updates the header via {@code checkVersion} — will therefore serialise-fail the
+ * first transaction if the writer commits first.
+ *
+ *
This is the shape of the failure we hit in the yaml-tests parallel harness: a "reader"
+ * transaction opens the {@code /__SYS/CATALOG} store, misses the cache, adds a wide read
+ * conflict, and then loses the commit race to a concurrent writer inside the same subspace.
+ * The paired positive control below ({@link #cacheHitOpenDoesNotConflictWithConcurrentRecordInsert()})
+ * shows that when the cache actually serves the open, the read conflict shrinks to a single
+ * key on {@code STORE_INFO_KEY} and no conflict occurs.
+ */
+ @Test
+ void cacheMissOpenConflictsWithConcurrentHeaderWrite() throws Exception {
+ fdb.setStoreStateCache(metaDataVersionStampCacheFactory.getCache(fdb));
+
+ // Bootstrap: create the store as cacheable, then invalidate the cache so the next opens
+ // are guaranteed to miss.
+ FDBRecordStore.Builder storeBuilder;
+ try (FDBRecordContext context = openContext()) {
+ openSimpleRecordStore(context);
+ assertTrue(recordStore.setStateCacheability(true));
+ storeBuilder = recordStore.asBuilder();
+ commit(context);
+ }
+ // Bump the meta-data version stamp to invalidate any cache entries. From here forward,
+ // opens will miss until at least one commits and repopulates the cache.
+ try (FDBRecordContext context = fdb.openContext()) {
+ context.setMetaDataVersionStamp();
+ commit(context);
+ }
+
+ // Open two contexts *concurrently* — both will fall through to loadStoreHeaderAsync, both
+ // will add a range read conflict that covers (at minimum) the header key.
+ try (FDBRecordContext readerContext = fdb.openContext(null, new FDBStoreTimer());
+ FDBRecordContext writerContext = fdb.openContext(null, new FDBStoreTimer())) {
+ // Pin both read versions *before* the writer commits, so the conflict has to be
+ // resolved at commit time rather than being masked by an updated read version.
+ readerContext.getReadVersion();
+ writerContext.getReadVersion();
+
+ // Reader: opens the store — this is where the SERIALIZABLE range read that will
+ // eventually serialise-fail us gets added to the read-conflict set.
+ FDBRecordStore reader = storeBuilder.copyBuilder().setContext(readerContext).open();
+ // Give the reader an explicit write so its commit actually runs conflict resolution
+ // (empty transactions skip it). Any unrelated key works.
+ readerContext.ensureActive().addWriteConflictKey(reader.recordsSubspace().pack(UUID.randomUUID()));
+
+ // Writer: open the same store (also a cache miss), then write the header
+ // (setHeaderUserField sets STORE_INFO_KEY) and commit first.
+ FDBRecordStore writer = storeBuilder.copyBuilder().setContext(writerContext).open();
+ writer.setHeaderUserField("miss-conflict-probe",
+ com.google.protobuf.ByteString.copyFromUtf8("probe-" + UUID.randomUUID()));
+ commit(writerContext);
+
+ // Now the reader must fail: its cache-miss read range on the header overlaps the
+ // writer's header write.
+ assertThrows(FDBExceptions.FDBStoreTransactionConflictException.class, readerContext::commit,
+ "cache-miss reader should conflict with concurrent header writer");
+ }
+ }
+
+ /**
+ * Positive control for {@link #cacheMissOpenConflictsWithConcurrentHeaderWrite()}: when the
+ * cache serves the open, the state-cache path adds only a point read conflict on
+ * {@code STORE_INFO_KEY} (see {@code FDBRecordStoreStateCacheEntry.handleCachedState}). A
+ * concurrent writer that inserts a record without touching the header will not conflict.
+ *
+ *
Together with the negative test above, this proves that the cache-miss ↔ range-read is
+ * the mechanism that causes the yaml-tests {@code /__SYS/CATALOG} conflicts: eliminating
+ * misses (e.g. by pinning the cache or by narrowing the miss path) would remove the
+ * conflict class entirely.
+ */
+ @Test
+ void cacheHitOpenDoesNotConflictWithConcurrentRecordInsert() throws Exception {
+ fdb.setStoreStateCache(metaDataVersionStampCacheFactory.getCache(fdb));
+
+ FDBRecordStore.Builder storeBuilder;
+ try (FDBRecordContext context = openContext()) {
+ openSimpleRecordStore(context);
+ assertTrue(recordStore.setStateCacheability(true));
+ storeBuilder = recordStore.asBuilder();
+ commit(context);
+ }
+ // Prime the cache with an open+commit so the entry is loaded and valid.
+ try (FDBRecordContext context = fdb.openContext(null, new FDBStoreTimer())) {
+ storeBuilder.copyBuilder().setContext(context).open();
+ commit(context);
+ }
+
+ try (FDBRecordContext readerContext = fdb.openContext(null, new FDBStoreTimer());
+ FDBRecordContext writerContext = fdb.openContext(null, new FDBStoreTimer())) {
+ readerContext.getReadVersion();
+ writerContext.getReadVersion();
+
+ // Reader: cache HIT — only STORE_INFO_KEY gets a point read conflict.
+ FDBRecordStore reader = storeBuilder.copyBuilder().setContext(readerContext).open();
+ // Force conflict resolution to actually run by giving the reader a write of its own,
+ // targeting a fresh unrelated key so it doesn't collide with the record we insert below.
+ readerContext.ensureActive().addWriteConflictKey(
+ reader.recordsSubspace().pack(Tuple.from("cache-hit-probe-" + UUID.randomUUID())));
+
+ // Writer: insert a new record. This writes only to /RECORDS/ and index
+ // keys — it does not touch STORE_INFO_KEY. Cache HIT reader's point read conflict on
+ // STORE_INFO_KEY does not overlap.
+ FDBRecordStore writer = storeBuilder.copyBuilder().setContext(writerContext).open();
+ writer.saveRecord(TestRecords1Proto.MySimpleRecord.newBuilder()
+ .setRecNo(4242L)
+ .setStrValueIndexed("cache-hit-probe")
+ .build());
+ commit(writerContext);
+
+ // Reader commits cleanly — its point read conflict on STORE_INFO_KEY doesn't overlap
+ // the writer's per-record writes.
+ commit(readerContext);
+ }
+ }
+
+ /**
+ * Companion to the two tests above that isolates the record-insert axis. It confirms — perhaps
+ * surprisingly — that two concurrent cache-miss opens that both only insert records (no header
+ * mutation) do not conflict with each other. The SERIALIZABLE
+ * {@code getRange(subspace.range(), 1)} that the miss path runs in
+ * {@code loadStoreHeaderAsync} appears to add a read conflict only up to (roughly) the header
+ * key, not the full subspace, so writes at {@code /RECORDS/} (which sit past the
+ * header keyspace) don't overlap.
+ *
+ *
This constrains the shape of the yaml-tests {@code /__SYS/CATALOG} failure: since the
+ * observed conflict range there covers the entire CATALOG subspace, the failure cannot be
+ * explained by "two cache-miss opens both inserting records" alone. There must be an
+ * additional mechanism — most likely one of the concurrent transactions implicitly writing
+ * {@code STORE_INFO_KEY} via {@code updateStoreHeaderAsync} on the {@code checkVersion} dirty
+ * path (format-version bump, metadata-version bump, or cacheability flip), or a wider write
+ * from a DDL constant action.
+ */
+ @Test
+ void concurrentCacheMissOpensBothInsertingRecordsDoNotConflict() throws Exception {
+ fdb.setStoreStateCache(metaDataVersionStampCacheFactory.getCache(fdb));
+
+ FDBRecordStore.Builder storeBuilder;
+ try (FDBRecordContext context = openContext()) {
+ openSimpleRecordStore(context);
+ assertTrue(recordStore.setStateCacheability(true));
+ storeBuilder = recordStore.asBuilder();
+ commit(context);
+ }
+ // Invalidate any cached entries so both concurrent opens below are guaranteed to miss.
+ try (FDBRecordContext context = fdb.openContext()) {
+ context.setMetaDataVersionStamp();
+ commit(context);
+ }
+
+ try (FDBRecordContext firstContext = fdb.openContext(null, new FDBStoreTimer());
+ FDBRecordContext secondContext = fdb.openContext(null, new FDBStoreTimer())) {
+ // Pin both read versions before either commits, so conflict resolution runs on both.
+ firstContext.getReadVersion();
+ secondContext.getReadVersion();
+
+ FDBRecordStore first = storeBuilder.copyBuilder().setContext(firstContext).open();
+ FDBRecordStore second = storeBuilder.copyBuilder().setContext(secondContext).open();
+
+ // Both insert distinct records — no header touched, distinct primary keys, distinct
+ // index entries (different str_value_indexed).
+ first.saveRecord(TestRecords1Proto.MySimpleRecord.newBuilder()
+ .setRecNo(1001L)
+ .setStrValueIndexed("first-writer")
+ .build());
+ second.saveRecord(TestRecords1Proto.MySimpleRecord.newBuilder()
+ .setRecNo(1002L)
+ .setStrValueIndexed("second-writer")
+ .build());
+
+ // Both commit cleanly — the cache-miss read conflict does not extend far enough to
+ // overlap a plain record-write, so this is not by itself the yaml-tests failure mode.
+ commit(secondContext);
+ commit(firstContext);
+ }
+ }
+
/**
* Verify that updating a header user field will be updated if the store state is cached.
*/
diff --git a/fdb-record-layer-core/src/testFixtures/java/com/apple/foundationdb/record/test/FDBDatabaseExtension.java b/fdb-record-layer-core/src/testFixtures/java/com/apple/foundationdb/record/test/FDBDatabaseExtension.java
index 144395abbc..a20fc4455b 100644
--- a/fdb-record-layer-core/src/testFixtures/java/com/apple/foundationdb/record/test/FDBDatabaseExtension.java
+++ b/fdb-record-layer-core/src/testFixtures/java/com/apple/foundationdb/record/test/FDBDatabaseExtension.java
@@ -43,6 +43,8 @@
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -84,6 +86,16 @@ public FDBDatabaseExtension() {
@Nonnull
private static final Executor threadPoolExecutor = TestExecutors.newThreadPool("fdb-record-layer-test");
+ // Override the per-factory scheduled executor so parallel tests don't fight over the
+ // JVM-wide single-thread ScheduledThreadPoolExecutor that MoreAsyncUtil installs by
+ // default. When the suite runs N tests concurrently and they each schedule deadline
+ // timers on a single shared thread, the timers fire late and trigger spurious
+ // DeadlineExceededException from places like AsyncLoadingCache (e.g.
+ // FDBDatabase.resolverStateCache during KeySpacePath.toTuple cleanup).
+ @Nonnull
+ private static final ScheduledExecutorService scheduledThreadPoolExecutor = Executors.newScheduledThreadPool(
+ Math.max(Runtime.getRuntime().availableProcessors(), 4),
+ new TestExecutors.TestThreadFactory("fdb-record-layer-test-scheduled"));
public static APIVersion getAPIVersion() {
String apiVersionStr = System.getProperty(API_VERSION_PROPERTY);
@@ -149,6 +161,7 @@ public FDBDatabaseFactory getDatabaseFactory() {
}
setupBlockingInAsyncDetection(databaseFactory);
databaseFactory.setExecutor(threadPoolExecutor);
+ databaseFactory.setScheduledExecutor(scheduledThreadPoolExecutor);
}
return databaseFactory;
}
diff --git a/fdb-record-layer-core/src/testFixtures/java/com/apple/foundationdb/record/test/TestKeySpace.java b/fdb-record-layer-core/src/testFixtures/java/com/apple/foundationdb/record/test/TestKeySpace.java
index a00012f24c..9903509e63 100644
--- a/fdb-record-layer-core/src/testFixtures/java/com/apple/foundationdb/record/test/TestKeySpace.java
+++ b/fdb-record-layer-core/src/testFixtures/java/com/apple/foundationdb/record/test/TestKeySpace.java
@@ -55,7 +55,16 @@ public class TestKeySpace {
.addSubdirectory(new DirectoryLayerDirectory(META_DATA_STORE, META_DATA_STORE))
.addSubdirectory(new DirectoryLayerDirectory(RAW_DATA, RAW_DATA))
.addSubdirectory(new DirectoryLayerDirectory(MULTI_RECORD_STORE, MULTI_RECORD_STORE)
- .addSubdirectory(new DirectoryLayerDirectory(STORE_PATH))
+ // STORE_PATH is a plain LONG-typed KeySpaceDirectory rather than a
+ // DirectoryLayerDirectory. It used to be a DirectoryLayerDirectory,
+ // which meant every unique test-supplied sub-store key (e.g. "store_0")
+ // triggered a fresh allocation against the JVM-wide root
+ // HighContentionAllocator. Under parallel test execution that racy
+ // allocator was hitting hasConflictAtRoot failures with sibling tests.
+ // A LONG here lets each test supply its own integer sub-store id
+ // (0, 1, 2, ...) which encodes directly into the tuple with no shared
+ // allocator involved.
+ .addSubdirectory(new KeySpaceDirectory(STORE_PATH, KeySpaceDirectory.KeyType.LONG))
)
.addSubdirectory(new DirectoryLayerDirectory(RESOLVER_MAPPING_REPLICATOR, RESOLVER_MAPPING_REPLICATOR)
.addSubdirectory(new KeySpaceDirectory("to", KeySpaceDirectory.KeyType.STRING, "to")
diff --git a/fdb-record-layer-core/src/testFixtures/java/com/apple/foundationdb/record/test/TestKeySpacePathManager.java b/fdb-record-layer-core/src/testFixtures/java/com/apple/foundationdb/record/test/TestKeySpacePathManager.java
index 7f953ee8da..8a9815a4e5 100644
--- a/fdb-record-layer-core/src/testFixtures/java/com/apple/foundationdb/record/test/TestKeySpacePathManager.java
+++ b/fdb-record-layer-core/src/testFixtures/java/com/apple/foundationdb/record/test/TestKeySpacePathManager.java
@@ -20,7 +20,9 @@
package com.apple.foundationdb.record.test;
+import com.apple.foundationdb.async.MoreAsyncUtil;
import com.apple.foundationdb.record.RecordCoreException;
+import com.apple.foundationdb.record.RecordCoreRetriableTransactionException;
import com.apple.foundationdb.record.logging.KeyValueLogMessage;
import com.apple.foundationdb.record.logging.LogMessageKeys;
import com.apple.foundationdb.record.provider.foundationdb.FDBDatabase;
@@ -89,6 +91,50 @@ public KeySpacePath createPath(String... pathElements) {
LogMessageKeys.KEY_SPACE_PATH, path));
}
assertTrue(paths.add(path), "UUID collision");
+
+ // Eagerly resolve the path under a JVM-wide lock. Path resolution goes through
+ // FDBDatabase.resolverStateCache, an AsyncLoadingCache with a hardcoded 5-second
+ // deadline (AsyncLoadingCache.DEFAULT_DEADLINE_TIME_MILLIS). When many parallel
+ // tests resolve directory-layer entries concurrently, the FDB-side work appears
+ // to contend with itself (retries triggered by conflicting commits on the
+ // resolver state) and the 5-second deadline fires from random call sites in
+ // test bodies and afterEach hooks. Serialising the resolution here means the
+ // resolver-state cache is warm by the time the test actually uses the path,
+ // so KeySpacePath.toTuple / FDBRecordStore.createOrOpen / deleteAllData calls
+ // hit the cache instead of racing on directory-layer reads.
+ //
+ // Use db.newRunner so we get the runner's standard retry loop for free.
+ // DeadlineExceededException extends LoggableException (not FDBException nor
+ // RecordCoreRetriableTransactionException) so the runner doesn't consider it
+ // retriable by default; wrap it explicitly to opt in.
+ //
+ // TODO: investigate whether the directory layer / LocatableResolver has a bug
+ // where concurrent resolutions of distinct keys fail to make progress on
+ // retry (rather than just being slow). If yes, the production-side fix would
+ // remove the need for this workaround entirely. See e.g.
+ // FDBDatabase.resolverStateCache, LocatableResolver.loadResolverState.
+ synchronized (TestKeySpacePathManager.class) {
+ final FDBRecordContextConfig.Builder config = FDBRecordContextConfig.newBuilder()
+ .setTransactionId("pathManager_createPath_" + UUID.randomUUID())
+ .setLogTransaction(true)
+ .setMdcContext(MDC.getCopyOfContextMap());
+ try (FDBDatabaseRunner runner = db.newRunner(config)) {
+ final KeySpacePath finalPath = path;
+ runner.run(context -> {
+ try {
+ finalPath.toTuple(context);
+ } catch (MoreAsyncUtil.DeadlineExceededException e) {
+ // The 5-second AsyncLoadingCache deadline occasionally fires
+ // under heavy parallel test load even though the work would
+ // succeed on retry. Wrap so the runner's retry loop picks it up.
+ throw new RecordCoreRetriableTransactionException(
+ "deadline exceeded resolving test key space path", e);
+ }
+ return null;
+ });
+ }
+ }
+
return path;
}
@@ -105,12 +151,22 @@ public void close() {
.setMdcContext(MDC.getCopyOfContextMap());
try (FDBDatabaseRunner runner = db.newRunner(config)) {
runner.run(context -> {
- for (KeySpacePath path : paths) {
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug(KeyValueLogMessage.of("deleting test key space path",
- LogMessageKeys.KEY_SPACE_PATH, path.toString(path.toTuple(context))));
+ try {
+ for (KeySpacePath path : paths) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(KeyValueLogMessage.of("deleting test key space path",
+ LogMessageKeys.KEY_SPACE_PATH, path.toString(path.toTuple(context))));
+ }
+ path.deleteAllData(context);
}
- path.deleteAllData(context);
+ } catch (MoreAsyncUtil.DeadlineExceededException e) {
+ // Same reason as in createPath: the 5-second AsyncLoadingCache
+ // deadline on FDBDatabase.resolverStateCache occasionally fires
+ // under heavy parallel test load. Wrap as a retriable exception
+ // so the runner's retry loop picks it up instead of failing the
+ // afterEach cleanup.
+ throw new RecordCoreRetriableTransactionException(
+ "deadline exceeded resolving test key space path during cleanup", e);
}
return null;
});
diff --git a/fdb-record-layer-lucene/src/test/java/com/apple/foundationdb/record/lucene/LuceneIndexMaintenanceTest.java b/fdb-record-layer-lucene/src/test/java/com/apple/foundationdb/record/lucene/LuceneIndexMaintenanceTest.java
index 607845f974..601d031814 100644
--- a/fdb-record-layer-lucene/src/test/java/com/apple/foundationdb/record/lucene/LuceneIndexMaintenanceTest.java
+++ b/fdb-record-layer-lucene/src/test/java/com/apple/foundationdb/record/lucene/LuceneIndexMaintenanceTest.java
@@ -72,6 +72,8 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -139,6 +141,9 @@
* Tests of the consistency of the Lucene Index.
*/
@Tag(Tags.RequiresFDB)
+// Runs SAME_THREAD because this class is FDB-heavy and saturates the shared cluster's Batch GRV
+// quota when run concurrently with itself or other FDB-heavy tests.
+@Execution(ExecutionMode.SAME_THREAD)
public class LuceneIndexMaintenanceTest extends FDBRecordStoreConcurrentTestBase {
private static final Logger LOGGER = LoggerFactory.getLogger(LuceneIndexMaintenanceTest.class);
diff --git a/fdb-record-layer-lucene/src/test/java/com/apple/foundationdb/record/lucene/LuceneIndexTest.java b/fdb-record-layer-lucene/src/test/java/com/apple/foundationdb/record/lucene/LuceneIndexTest.java
index bff8ec1788..0ee2e7679e 100644
--- a/fdb-record-layer-lucene/src/test/java/com/apple/foundationdb/record/lucene/LuceneIndexTest.java
+++ b/fdb-record-layer-lucene/src/test/java/com/apple/foundationdb/record/lucene/LuceneIndexTest.java
@@ -101,6 +101,8 @@
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -226,6 +228,11 @@
*/
@SuppressWarnings({"resource", "SameParameterValue"})
@Tag(Tags.RequiresFDB)
+// Runs SAME_THREAD because this class (and its subclass below) is FDB-heavy and saturates the
+// shared cluster's Batch GRV quota when run concurrently with itself or other FDB-heavy tests
+// (LuceneIndexMaintenanceTest, AgilityContextTest). @Execution is not @Inherited, so the nested
+// LuceneIndexWithLuceneAsyncToSyncTest below repeats this annotation.
+@Execution(ExecutionMode.SAME_THREAD)
public class LuceneIndexTest extends FDBLuceneTestBase {
private static final Logger LOGGER = LoggerFactory.getLogger(LuceneIndexTest.class);
@@ -6313,6 +6320,7 @@ void luceneIndexAttributesNotOptimizedForMutualIndexing() {
/**
* A version of the tests that runs with the new version of asyncToSync.
*/
+ @Execution(ExecutionMode.SAME_THREAD)
public static class LuceneIndexWithLuceneAsyncToSyncTest extends LuceneIndexTest {
@Override
protected RecordLayerPropertyStorage.Builder addDefaultProps(final RecordLayerPropertyStorage.Builder props) {
diff --git a/fdb-record-layer-lucene/src/test/java/com/apple/foundationdb/record/lucene/directory/AgilityContextTest.java b/fdb-record-layer-lucene/src/test/java/com/apple/foundationdb/record/lucene/directory/AgilityContextTest.java
index 1c8635802a..0201c33f51 100644
--- a/fdb-record-layer-lucene/src/test/java/com/apple/foundationdb/record/lucene/directory/AgilityContextTest.java
+++ b/fdb-record-layer-lucene/src/test/java/com/apple/foundationdb/record/lucene/directory/AgilityContextTest.java
@@ -45,6 +45,8 @@
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
@@ -74,6 +76,9 @@
* Tests for AgilityContext.
*/
@Tag(Tags.RequiresFDB)
+// Runs SAME_THREAD because this class commits to the shared FDB cluster and races (write-write
+// conflicts, GRV throttling) with other FDB-heavy lucene tests when run concurrently.
+@Execution(ExecutionMode.SAME_THREAD)
class AgilityContextTest extends FDBRecordStoreTestBase {
enum AgilityContextType { AGILE, NON_AGILE, READ_ONLY }
@@ -626,6 +631,14 @@ void testReadOnlyHasCallerGrv() throws Exception {
byte[] key;
FDBRecordContext earlyContext = openContext();
+ // Pin earlyContext's read version *now*, before laterContext writes. Otherwise the GRV
+ // is deferred until the first FDB read against earlyContext — and if path.toSubspace()
+ // below is served from an in-JVM directory-layer cache (which happens whenever other
+ // tests in the same JVM have already warmed that resolver), no FDB round-trip occurs
+ // and earlyContext's GRV ends up being grabbed later — after laterContext commits —
+ // via `callerContext.getReadVersion()` inside ReadOnlyNonAgileContext.ctor. That would
+ // let earlyContext see the "Hello" write and break the invariant this test is checking.
+ earlyContext.getReadVersion();
final Subspace subspace = path.toSubspace(earlyContext);
key = subspace.pack(Tuple.from("something"));
diff --git a/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/Options.java b/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/Options.java
index a159ee60d9..ad78486be4 100644
--- a/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/Options.java
+++ b/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/Options.java
@@ -257,6 +257,15 @@ public enum Name {
* A boolean indicating whether to (attempt to) compress records when saving.
*/
COMPRESS_WHEN_SERIALIZING,
+
+ /**
+ * A boolean indicating whether the underlying {@code FDBRecordContext} should be configured to
+ * populate its list of not-committed conflicting keys after a failed commit. Useful for
+ * diagnosing serialization failures — when enabled, the connection's commit failure message
+ * will include the concrete FDB key ranges that conflicted. Off by default (there is a small
+ * extra read against the FDB special-key-space on commit failure when it is on).
+ */
+ REPORT_CONFLICTING_KEYS,
}
public enum IndexFetchMethod {
@@ -300,6 +309,7 @@ public enum IndexFetchMethod {
builder.put(Name.ENCRYPT_WHEN_SERIALIZING, false);
builder.put(Name.ENCRYPTION_KEY_PASSWORD, "");
builder.put(Name.COMPRESS_WHEN_SERIALIZING, true);
+ builder.put(Name.REPORT_CONFLICTING_KEYS, false);
OPTIONS_DEFAULT_VALUES = builder.build();
}
@@ -560,6 +570,7 @@ private static Map> makeContracts() {
data.put(Name.ENCRYPTION_KEY_ENTRY_LIST, List.of(new OrderedCollectionContract<>(TypeContract.stringType())));
data.put(Name.ENCRYPTION_KEY_PASSWORD, List.of(TypeContract.nullableStringType()));
data.put(Name.COMPRESS_WHEN_SERIALIZING, List.of(TypeContract.booleanType()));
+ data.put(Name.REPORT_CONFLICTING_KEYS, List.of(TypeContract.booleanType()));
return Collections.unmodifiableMap(data);
}
diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalConnection.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalConnection.java
index bb9c0671f5..ed4ed623fa 100644
--- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalConnection.java
+++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalConnection.java
@@ -20,10 +20,12 @@
package com.apple.foundationdb.relational.recordlayer;
+import com.apple.foundationdb.Range;
import com.apple.foundationdb.annotation.API;
import com.apple.foundationdb.record.ExecuteProperties;
import com.apple.foundationdb.record.IsolationLevel;
import com.apple.foundationdb.record.RecordCoreException;
+import com.apple.foundationdb.record.provider.foundationdb.FDBRecordContext;
import com.apple.foundationdb.relational.api.EmbeddedRelationalDriver;
import com.apple.foundationdb.relational.api.EmbeddedRelationalStruct;
import com.apple.foundationdb.relational.api.Options;
@@ -48,11 +50,15 @@
import com.apple.foundationdb.relational.recordlayer.metric.StoreTimerMetricCollector;
import com.apple.foundationdb.relational.recordlayer.structuredsql.expression.ExpressionFactoryImpl;
import com.apple.foundationdb.relational.recordlayer.structuredsql.statement.StatementBuilderFactoryImpl;
+import com.apple.foundationdb.relational.recordlayer.util.ConflictKeyFormatter;
import com.apple.foundationdb.relational.recordlayer.util.ExceptionUtil;
import com.apple.foundationdb.relational.util.SpotBugsSuppressWarnings;
import com.apple.foundationdb.relational.util.Supplier;
import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.net.URI;
@@ -62,6 +68,7 @@
import java.sql.SQLWarning;
import java.sql.Struct;
import java.util.Arrays;
+import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
@@ -81,6 +88,8 @@
@API(API.Status.EXPERIMENTAL)
public class EmbeddedRelationalConnection implements RelationalConnection {
+ private static final Logger LOGGER = LoggerFactory.getLogger(EmbeddedRelationalConnection.class);
+
/**
* Chose a transaction level that we *pretend* to support (Rather than throw an exception that stops
* all processing).
@@ -199,6 +208,10 @@ void commitInternal() throws SQLException {
getTransaction().commit();
} catch (RuntimeException | RelationalException re) {
err = ExceptionUtil.toRelationalException(re);
+ // Best-effort: if the underlying FDB context was configured with
+ // Options.Name.REPORT_CONFLICTING_KEYS, pull the not-committed conflict ranges out and
+ // enrich the exception (and log) so we can see which key(s) actually collided.
+ err = attachConflictingKeys(err);
}
try {
getTransaction().close();
@@ -215,6 +228,45 @@ void commitInternal() throws SQLException {
}
}
+ /**
+ * If the transaction's FDB context had {@code reportConflictingKeys} enabled and the commit
+ * failed with NOT_COMMITTED, rewrap the exception with a message that includes the concrete
+ * conflict ranges (so they appear in every stack trace) and also attach them to the exception's
+ * context map. Never throws — instrumentation must not itself fail the commit path.
+ *
+ * @param err the original exception; returned unchanged if extraction fails or reporting is off
+ * @return the (possibly-rewrapped) exception with conflict information baked into its message
+ */
+ @Nonnull
+ private RelationalException attachConflictingKeys(@Nonnull RelationalException err) {
+ try {
+ final Transaction txn = transaction;
+ if (txn == null) {
+ return err;
+ }
+ final FDBRecordContext ctx = txn.unwrap(FDBRecordContext.class);
+ final List ranges = ctx.getNotCommittedConflictingKeys();
+ if (ranges == null) {
+ return err; // reporting was not enabled, or commit did not fail with NOT_COMMITTED
+ }
+ final String rendered = ConflictKeyFormatter.formatRanges(ranges);
+ final String enrichedMessage = (err.getMessage() == null ? "" : err.getMessage())
+ + " [conflicting_keys_count=" + ranges.size() + ", conflicting_keys=" + rendered + "]";
+ LOGGER.warn("FDB transaction commit failed with conflicting keys: count={} ranges={}",
+ ranges.size(), rendered);
+ final RelationalException rewrapped = new RelationalException(enrichedMessage, err.getErrorCode(), err);
+ rewrapped.addContext("conflicting_keys", rendered);
+ rewrapped.addContext("conflicting_keys_count", ranges.size());
+ // Preserve any context that was already on the original exception.
+ rewrapped.withContext(err.getContext());
+ return rewrapped;
+ } catch (Exception e) {
+ // Never let diagnostic extraction mask the real error.
+ LOGGER.debug("Failed to extract conflicting keys from failed commit", e);
+ return err;
+ }
+ }
+
@Override
public void rollback() throws SQLException {
checkOpen();
diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerTransactionManager.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerTransactionManager.java
index 8cf472ece2..92e5a70996 100644
--- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerTransactionManager.java
+++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerTransactionManager.java
@@ -66,6 +66,10 @@ private FDBRecordContextConfig getFDBRecordContextConfig(@Nonnull Options option
if (transactionTimeout != null) {
builder.setTransactionTimeoutMillis(transactionTimeout);
}
+ Boolean reportConflictingKeys = options.getOption(Options.Name.REPORT_CONFLICTING_KEYS);
+ if (Boolean.TRUE.equals(reportConflictingKeys)) {
+ builder.setReportConflictingKeys(true);
+ }
return builder.build();
}
}
diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RelationalKeyspaceProvider.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RelationalKeyspaceProvider.java
index ef82783624..5fa6769c6b 100644
--- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RelationalKeyspaceProvider.java
+++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RelationalKeyspaceProvider.java
@@ -188,7 +188,14 @@ private static KeySpaceDirectory getSystemDirectory() {
.addSubdirectory(new KeySpaceDirectory(INTERNING_LAYER, KeySpaceDirectory.KeyType.STRING, INTERNING_LAYER_VALUE));
}
- public void registerDomainIfNotExists(@Nonnull String domainName) {
+ public synchronized void registerDomainIfNotExists(@Nonnull String domainName) {
+ // Synchronized because this mutates the singleton KeySpace's tree via
+ // addSubdirectory. Without the lock, two concurrent callers can both observe "not
+ // present" and both add the same domain, leaving the tree with two subdirectories
+ // matching the same name — every subsequent path resolution then throws
+ // "<...> is ambigous" from KeySpaceUtils#matchPathToSubdirectories. That surfaces
+ // once multiple test-class @BeforeAll hooks (or parallel FRL constructions) hit
+ // this on the same JVM.
final var keySpaceRoot = getKeySpace().getRoot();
final var exists = keySpaceRoot.getSubdirectories().stream()
.map(KeySpaceDirectory::getName)
diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/catalog/RecordLayerStoreCatalog.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/catalog/RecordLayerStoreCatalog.java
index 35e93a0574..64a193b706 100644
--- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/catalog/RecordLayerStoreCatalog.java
+++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/catalog/RecordLayerStoreCatalog.java
@@ -180,8 +180,14 @@ StoreCatalog initialize(@Nonnull final Transaction createTxn, @Nonnull final Sch
schemaTemplateCatalog.createTemplate(createTxn, this.catalogSchemaTemplate);
}
this.schemaTemplateCatalog = schemaTemplateCatalog;
- // Persist our hard-coded catalog schema.
- saveSchema(createTxn, this.catalogSchema, true);
+ // Persist our hard-coded catalog schema — but only if it isn't already there. Every
+ // FRL construction and every proactive test-time initialiser calls this method;
+ // writing the same schema row on every call turned every parallel init into a
+ // write-write commit conflict on the catalog table. Gate on doesSchemaExist so the
+ // second and subsequent inits become read-only and can commit concurrently.
+ if (!doesSchemaExist(createTxn, URI.create(this.catalogSchema.getDatabaseName()), this.catalogSchema.getName())) {
+ saveSchema(createTxn, this.catalogSchema, true);
+ }
} catch (RecordCoreStorageException ex) {
throw ExceptionUtil.toRelationalException(ex);
}
diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ConflictKeyFormatter.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ConflictKeyFormatter.java
new file mode 100644
index 0000000000..da82689411
--- /dev/null
+++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ConflictKeyFormatter.java
@@ -0,0 +1,103 @@
+/*
+ * ConflictKeyFormatter.java
+ *
+ * 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.
+ */
+
+package com.apple.foundationdb.relational.recordlayer.util;
+
+import com.apple.foundationdb.Range;
+import com.apple.foundationdb.annotation.API;
+import com.apple.foundationdb.tuple.ByteArrayUtil;
+import com.apple.foundationdb.tuple.Tuple;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Utilities for rendering FDB conflict ranges (returned by
+ * {@code FDBRecordContext.getNotCommittedConflictingKeys()}) into strings suitable for logging
+ * and inclusion in exception messages.
+ *
+ *
For each key we attempt to parse it as an FDB {@link Tuple}; if that fails we fall back to
+ * {@link ByteArrayUtil#printable(byte[])} which produces the same escaping FDB tooling uses (e.g.
+ * {@code \xff/metadataVersion}).
+ */
+@API(API.Status.EXPERIMENTAL)
+public final class ConflictKeyFormatter {
+
+ private ConflictKeyFormatter() {
+ }
+
+ /**
+ * Render a list of conflict ranges as a compact human-readable string.
+ * Returns {@code "[]"} for an empty list, {@code "null"} for null.
+ *
+ * @param ranges the conflict ranges, or null
+ * @return a comma-separated description of the ranges enclosed in brackets
+ */
+ @Nonnull
+ public static String formatRanges(@Nullable List ranges) {
+ if (ranges == null) {
+ return "null";
+ }
+ if (ranges.isEmpty()) {
+ return "[]";
+ }
+ return ranges.stream()
+ .map(ConflictKeyFormatter::formatRange)
+ .collect(Collectors.joining(", ", "[", "]"));
+ }
+
+ /**
+ * Render a single conflict range as {@code "begin..end"} using {@link #formatKey(byte[])} for each bound.
+ * @param range the range
+ * @return a printable description
+ */
+ @Nonnull
+ public static String formatRange(@Nonnull Range range) {
+ return formatKey(range.begin) + ".." + formatKey(range.end);
+ }
+
+ /**
+ * Render a single FDB key. First attempts to decode as a {@link Tuple}; if that fails, falls back to
+ * {@link ByteArrayUtil#printable(byte[])}.
+ * @param key the raw key bytes, may be null
+ * @return a printable description
+ */
+ @Nonnull
+ public static String formatKey(@Nullable byte[] key) {
+ if (key == null) {
+ return "null";
+ }
+ if (key.length == 0) {
+ return "\"\"";
+ }
+ // System-key-space keys start with 0xff; don't try to Tuple-decode them (they aren't tuples).
+ if ((key[0] & 0xff) != 0xff) {
+ try {
+ final Tuple t = Tuple.fromBytes(key);
+ return t.toString();
+ } catch (RuntimeException ignored) {
+ // Fall through to raw byte rendering.
+ }
+ }
+ return ByteArrayUtil.printable(key);
+ }
+}
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/SqlInsertTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/SqlInsertTest.java
index 64bf2a3a16..3ad776d668 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/SqlInsertTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/SqlInsertTest.java
@@ -41,8 +41,8 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
-import java.sql.DriverManager;
import java.util.Map;
+import java.net.URI;
public class SqlInsertTest {
@@ -52,7 +52,7 @@ public class SqlInsertTest {
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(BasicMetadataTest.class,
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, BasicMetadataTest.class,
"""
CREATE TABLE simple (rest_no bigint, name string, primary key(rest_no))
CREATE TYPE AS STRUCT location (address string, latitude string, longitude string)
@@ -63,7 +63,7 @@ CREATE TABLE with_loc (rest_no bigint, name string, loc location, primary key(re
@ParameterizedTest
@ValueSource(booleans = {true, false})
void canInsertUsingSqlSyntaxAndSingleQuotes(boolean useNamed) throws Exception {
- try (RelationalConnection conn = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
conn.setSchema(database.getSchemaName());
try (RelationalStatement stmt = conn.createStatement()) {
@@ -83,7 +83,7 @@ void canInsertUsingSqlSyntaxAndSingleQuotes(boolean useNamed) throws Exception {
@Test
void canInsertStructUnnamed() throws Exception {
- try (RelationalConnection conn = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
conn.setSchema(database.getSchemaName());
try (RelationalStatement stmt = conn.createStatement()) {
@@ -103,7 +103,7 @@ void canInsertStructUnnamed() throws Exception {
@Test
void canInsertStructNamed() throws Exception {
- try (RelationalConnection conn = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
conn.setSchema(database.getSchemaName());
try (RelationalStatement stmt = conn.createStatement()) {
@@ -123,7 +123,7 @@ void canInsertStructNamed() throws Exception {
@Test
void cannotInsertWithInsertRuleDisabled() throws Exception {
- try (RelationalConnection conn = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
// Turn off the planner rule that allows for inserts to be planned
conn.setOption(Options.Name.DISABLED_PLANNER_RULES, ImmutableSet.of(ImplementInsertRule.class.getSimpleName()));
conn.setSchema(database.getSchemaName());
@@ -143,7 +143,7 @@ void cannotInsertWithInsertRuleDisabled() throws Exception {
@Test
@Disabled()
void canInsertStructWithStructFieldsNamed() throws Exception {
- try (RelationalConnection conn = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
conn.setSchema(database.getSchemaName());
try (RelationalStatement stmt = conn.createStatement()) {
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/DriverManagerTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/DriverManagerTest.java
index 7474d9a106..7113ec0633 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/DriverManagerTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/DriverManagerTest.java
@@ -25,6 +25,7 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
@@ -34,6 +35,17 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
+/**
+ * This is the only test in the relational-core test suite that interacts with
+ * {@link DriverManager} directly. Every other test gets its driver via
+ * {@code relationalExtension.getDriver()}.
+ *
+ * Marked {@link Isolated} because {@code @BeforeEach} clears the entire JVM-wide DriverManager
+ * registry. Without isolation, running this concurrently with any other test would race and pull
+ * the driver out from under it. {@link Isolated} tells JUnit Jupiter to suspend all other tests
+ * while this class runs.
+ */
+@Isolated
class DriverManagerTest {
RelationalDriver driver1 = Mockito.mock(RelationalDriver.class);
RelationalDriver driver2 = Mockito.mock(RelationalDriver.class);
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/RelationalConnectionTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/RelationalConnectionTest.java
index ba708364bd..e5a4d71b97 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/RelationalConnectionTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/RelationalConnectionTest.java
@@ -29,8 +29,8 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
+import java.net.URI;
import java.sql.Connection;
-import java.sql.DriverManager;
import java.sql.SQLException;
class RelationalConnectionTest {
@@ -40,26 +40,27 @@ class RelationalConnectionTest {
public final EmbeddedRelationalExtension relationalExtension = new EmbeddedRelationalExtension();
@Test
- void wrongScheme() {
- RelationalAssertions.assertThrowsSqlException(() -> DriverManager.getConnection("foo"))
- .hasErrorCode(ErrorCode.UNABLE_TO_ESTABLISH_SQL_CONNECTION);
-
- RelationalAssertions.assertThrowsSqlException(() -> DriverManager.getConnection("foo:foo"))
- .hasErrorCode(ErrorCode.UNABLE_TO_ESTABLISH_SQL_CONNECTION);
-
- RelationalAssertions.assertThrowsSqlException(() -> DriverManager.getConnection("jdbc:foo"))
- .hasErrorCode(ErrorCode.UNABLE_TO_ESTABLISH_SQL_CONNECTION);
-
- RelationalAssertions.assertThrowsSqlException(() -> DriverManager.getConnection("jdbc:embed"))
- .hasErrorCode(ErrorCode.UNABLE_TO_ESTABLISH_SQL_CONNECTION);
-
- RelationalAssertions.assertThrowsSqlException(() -> DriverManager.getConnection("jdbc:embed:/i_am_not_a_database"))
+ void wrongScheme() throws SQLException {
+ // The JDBC Driver contract says connect() returns null for URLs the driver doesn't
+ // accept (so DriverManager can chain to another driver). Calling driver.connect() with
+ // no-scheme / wrong-scheme URLs directly therefore returns null rather than throwing —
+ // which is correct per-spec. (Going through DriverManager.getConnection() would convert
+ // the null into a "No suitable driver found" SQLException; we deliberately avoid that
+ // here because DriverManagerTest is the single test that exercises DriverManager.)
+ Assertions.assertThat(relationalExtension.getDriver().connect(URI.create("foo"))).isNull();
+ Assertions.assertThat(relationalExtension.getDriver().connect(URI.create("foo:foo"))).isNull();
+ Assertions.assertThat(relationalExtension.getDriver().connect(URI.create("jdbc:foo"))).isNull();
+ Assertions.assertThat(relationalExtension.getDriver().connect(URI.create("jdbc:embed"))).isNull();
+
+ // The URL IS accepted (it starts with "jdbc:embed:"), but the path resolves to a
+ // database that doesn't exist. The driver throws here rather than returning null.
+ RelationalAssertions.assertThrowsSqlException(() -> relationalExtension.getDriver().connect(URI.create("jdbc:embed:/i_am_not_a_database")))
.hasErrorCode(ErrorCode.UNDEFINED_DATABASE);
}
@Test
void missingLeadingSlash() {
- RelationalAssertions.assertThrowsSqlException(() -> DriverManager.getConnection("jdbc:embed:i_am_not_a_database"))
+ RelationalAssertions.assertThrowsSqlException(() -> relationalExtension.getDriver().connect(URI.create("jdbc:embed:i_am_not_a_database")))
.hasErrorCode(ErrorCode.UNDEFINED_DATABASE)
.containsInMessage("")
.doesNotContainInMessage("")
@@ -69,7 +70,7 @@ void missingLeadingSlash() {
@Test
void setWrongSchema() throws SQLException {
- try (final var conn = DriverManager.getConnection("jdbc:embed:/__SYS")) {
+ try (final var conn = relationalExtension.getDriver().connect(URI.create("jdbc:embed:/__SYS"))) {
RelationalAssertions.assertThrowsSqlException(() -> conn.setSchema("foo"))
.hasErrorCode(ErrorCode.UNDEFINED_SCHEMA);
}
@@ -77,26 +78,26 @@ void setWrongSchema() throws SQLException {
@Test
void canConnectDirectlyToSchema() throws SQLException {
- try (final var conn = DriverManager.getConnection("jdbc:embed:/__SYS?schema=CATALOG")) {
+ try (final var conn = relationalExtension.getDriver().connect(URI.create("jdbc:embed:/__SYS?schema=CATALOG"))) {
Assertions.assertThat(conn.getSchema()).isEqualTo("CATALOG");
}
}
@Test
void connectDirectlyToNonexistentDatabaseBlowsUp() {
- RelationalAssertions.assertThrowsSqlException(() -> DriverManager.getConnection("jdbc:embed:/notADatabase?schema=CATALOG"))
+ RelationalAssertions.assertThrowsSqlException(() -> relationalExtension.getDriver().connect(URI.create("jdbc:embed:/notADatabase?schema=CATALOG")))
.hasErrorCode(ErrorCode.UNDEFINED_DATABASE);
}
@Test
void connectDirectlyToNonexistentSchemaBlowsUp() {
- RelationalAssertions.assertThrowsSqlException(() -> DriverManager.getConnection("jdbc:embed:/__SYS?schema=noSuchSchema"))
+ RelationalAssertions.assertThrowsSqlException(() -> relationalExtension.getDriver().connect(URI.create("jdbc:embed:/__SYS?schema=noSuchSchema")))
.hasErrorCode(ErrorCode.UNDEFINED_SCHEMA);
}
@Test
void setIsolationLevel() throws SQLException {
- try (RelationalConnection conn = DriverManager.getConnection("jdbc:embed:/__SYS").unwrap(RelationalConnection.class)) {
+ try (RelationalConnection conn = relationalExtension.getDriver().connect(URI.create("jdbc:embed:/__SYS")).unwrap(RelationalConnection.class)) {
// Default isolation level
Assertions.assertThat(conn.getTransactionIsolation()).isEqualTo(Connection.TRANSACTION_SERIALIZABLE);
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlStatementParsingTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlStatementParsingTest.java
index 0b544bfddd..a789298462 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlStatementParsingTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlStatementParsingTest.java
@@ -106,12 +106,12 @@ public class DdlStatementParsingTest {
@RegisterExtension
@Order(2)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(DdlStatementParsingTest.class, TestSchemas.books(),
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, DdlStatementParsingTest.class, TestSchemas.books(),
Options.builder().withOption(Options.Name.CASE_SENSITIVE_IDENTIFIERS, true).build(), null);
@RegisterExtension
@Order(3)
- public final RelationalConnectionRule connection = new RelationalConnectionRule(database::getConnectionUri)
+ public final RelationalConnectionRule connection = new RelationalConnectionRule(relationalExtension, database::getConnectionUri)
.withSchema("TEST_SCHEMA")
.withOptions(Options.builder().withOption(Options.Name.CASE_SENSITIVE_IDENTIFIERS, true).build());
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java
index e17f22f4d4..2b1edf5738 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java
@@ -78,11 +78,11 @@ public class IndexTest {
@RegisterExtension
@Order(2)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(DdlStatementParsingTest.class, TestSchemas.books());
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, DdlStatementParsingTest.class, TestSchemas.books());
@RegisterExtension
@Order(3)
- public final RelationalConnectionRule connection = new RelationalConnectionRule(database::getConnectionUri)
+ public final RelationalConnectionRule connection = new RelationalConnectionRule(relationalExtension, database::getConnectionUri)
.withSchema("TEST_SCHEMA");
@BeforeAll
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/SqlFunctionTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/SqlFunctionTest.java
index 15fc4e46ca..54c29057cd 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/SqlFunctionTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/SqlFunctionTest.java
@@ -64,11 +64,11 @@ public class SqlFunctionTest {
@RegisterExtension
@Order(2)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(DdlStatementParsingTest.class, TestSchemas.books());
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, DdlStatementParsingTest.class, TestSchemas.books());
@RegisterExtension
@Order(3)
- public final RelationalConnectionRule connection = new RelationalConnectionRule(database::getConnectionUri)
+ public final RelationalConnectionRule connection = new RelationalConnectionRule(relationalExtension, database::getConnectionUri)
.withSchema("TEST_SCHEMA");
@BeforeAll
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/autotest/cases/DirectPrimaryKeyScanTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/autotest/cases/DirectPrimaryKeyScanTest.java
index 8d7a6c2cad..710b55f29a 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/autotest/cases/DirectPrimaryKeyScanTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/autotest/cases/DirectPrimaryKeyScanTest.java
@@ -46,7 +46,6 @@
import org.junit.jupiter.api.extension.RegisterExtension;
import java.net.URI;
-import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
@@ -78,7 +77,7 @@ public class DirectPrimaryKeyScanTest {
public Connector relationalConnector = new Connector() {
@Override
public RelationalConnection connect(URI dbUri) throws SQLException {
- return DriverManager.getConnection("jdbc:embed:" + dbUri.getPath()).unwrap(RelationalConnection.class);
+ return relational.getDriver().connect(URI.create("jdbc:embed:" + dbUri.getPath())).unwrap(RelationalConnection.class);
}
@Override
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/autotest/cases/KnownPkGetTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/autotest/cases/KnownPkGetTest.java
index 576b70408c..9df702c5c0 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/autotest/cases/KnownPkGetTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/autotest/cases/KnownPkGetTest.java
@@ -46,7 +46,6 @@
import org.junit.jupiter.api.extension.RegisterExtension;
import java.net.URI;
-import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
@@ -82,7 +81,7 @@ public KnownPkGetTest() {
public Connector relationalConnector = new Connector() {
@Override
public RelationalConnection connect(URI dbUri) throws SQLException {
- return DriverManager.getConnection("jdbc:embed:" + dbUri.getPath()).unwrap(RelationalConnection.class);
+ return relational.getDriver().connect(URI.create("jdbc:embed:" + dbUri.getPath())).unwrap(RelationalConnection.class);
}
@Override
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/jdbc/JDBCEmbedDriverTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/jdbc/JDBCEmbedDriverTest.java
index 9924ae453e..83383568fa 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/jdbc/JDBCEmbedDriverTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/jdbc/JDBCEmbedDriverTest.java
@@ -20,38 +20,22 @@
package com.apple.foundationdb.relational.jdbc;
-import com.apple.foundationdb.record.provider.foundationdb.APIVersion;
-import com.apple.foundationdb.record.provider.foundationdb.FDBDatabase;
-import com.apple.foundationdb.record.provider.foundationdb.FDBDatabaseFactory;
-import com.apple.foundationdb.test.FDBTestEnvironment;
-import com.apple.foundationdb.relational.api.EmbeddedRelationalDriver;
-import com.apple.foundationdb.relational.api.Options;
import com.apple.foundationdb.relational.api.RelationalDriver;
-import com.apple.foundationdb.relational.api.catalog.StoreCatalog;
-import com.apple.foundationdb.relational.api.exceptions.RelationalException;
-import com.apple.foundationdb.relational.recordlayer.DirectFdbConnection;
-import com.apple.foundationdb.relational.recordlayer.RecordLayerConfig;
-import com.apple.foundationdb.relational.recordlayer.RecordLayerEngine;
+import com.apple.foundationdb.relational.recordlayer.EmbeddedRelationalExtension;
import com.apple.foundationdb.relational.recordlayer.RelationalKeyspaceProvider;
-import com.apple.foundationdb.relational.recordlayer.catalog.StoreCatalogProvider;
-import com.apple.foundationdb.relational.recordlayer.ddl.RecordLayerMetadataOperationsFactory;
-import com.apple.foundationdb.relational.recordlayer.query.cache.RelationalPlanCache;
import com.apple.foundationdb.relational.util.BuildVersion;
+import com.apple.foundationdb.relational.utils.CatalogOperations;
-import com.codahale.metrics.MetricRegistry;
-import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
-import java.sql.Driver;
-import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Statement;
import java.sql.Types;
-import java.util.Collections;
/**
* Run some simple Statement updates/executes against the JDBC Embed JDBC Driver.
@@ -61,54 +45,15 @@
*/
public class JDBCEmbedDriverTest {
- private static RelationalDriver driver;
-
- @BeforeAll
- public static void beforeAll() throws SQLException, RelationalException {
- RelationalKeyspaceProvider.instance().registerDomainIfNotExists("FRL");
-
- RecordLayerConfig rlCfg = RecordLayerConfig.getDefault();
- //here we are extending the StorageCluster so that we can track which internal Databases were
- // connected to and we can validate that they were all closed properly
-
- // This needs to be done prior to the first call to factory.getDatabase()
- FDBDatabaseFactory.instance().setAPIVersion(APIVersion.API_VERSION_7_1);
-
- final FDBDatabase database = FDBDatabaseFactory.instance().getDatabase(FDBTestEnvironment.randomClusterFile());
- StoreCatalog storeCatalog;
- try (var txn = new DirectFdbConnection(database).getTransactionManager().createTransaction(Options.NONE)) {
- storeCatalog = StoreCatalogProvider.getCatalog(txn, RelationalKeyspaceProvider.instance().getKeySpace());
- txn.commit();
- }
-
- RecordLayerMetadataOperationsFactory ddlFactory = RecordLayerMetadataOperationsFactory.defaultFactory()
- .setBaseKeySpace(RelationalKeyspaceProvider.instance().getKeySpace())
- .setRlConfig(rlCfg)
- .setStoreCatalog(storeCatalog)
- .build();
- driver = new EmbeddedRelationalDriver(RecordLayerEngine.makeEngine(
- rlCfg,
- Collections.singletonList(database),
- RelationalKeyspaceProvider.instance().getKeySpace(),
- storeCatalog,
- new MetricRegistry(),
- ddlFactory,
- RelationalPlanCache.buildWithDefaults()));
- DriverManager.registerDriver(driver); //register the engine driver
- }
-
- static Driver getDriver() throws SQLException {
- // Use ANY valid URl to get hold of the driver. When we 'connect' we'll
- // more specific about where we want to connect to.
- return DriverManager.getDriver("jdbc:embed:" + SYSDBPATH);
- }
+ @RegisterExtension
+ @Order(0)
+ static final EmbeddedRelationalExtension relationalExtension = new EmbeddedRelationalExtension();
private static final String SYSDBPATH = "/" + RelationalKeyspaceProvider.SYS;
private static final String TESTDB = "/FRL/jdbc_test_db";
- @AfterAll
- public static void afterAll() throws SQLException {
- DriverManager.deregisterDriver(driver);
+ private static RelationalDriver getDriver() {
+ return relationalExtension.getDriver();
}
@Test
@@ -117,17 +62,17 @@ public void testGetPropertyInfo() throws SQLException {
}
@Test
- public void testGetMajorVersion() throws SQLException, RelationalException {
+ public void testGetMajorVersion() throws Exception {
Assertions.assertEquals(getDriver().getMajorVersion(), BuildVersion.getInstance().getMajorVersion());
}
@Test
- public void testGetMinorVersion() throws SQLException, RelationalException {
+ public void testGetMinorVersion() throws Exception {
Assertions.assertEquals(getDriver().getMinorVersion(), BuildVersion.getInstance().getMinorVersion());
}
@Test
- public void testJDBCCompliant() throws SQLException {
+ public void testJDBCCompliant() {
Assertions.assertFalse(getDriver().jdbcCompliant());
}
@@ -139,8 +84,13 @@ public void testGetParentLogger() {
@Test
public void simpleStatement() throws SQLException {
- var jdbcStr = "jdbc:embed:" + SYSDBPATH + "?schema=" + RelationalKeyspaceProvider.CATALOG;
- try (final var connection = getDriver().connect(jdbcStr, null)) {
+ // Register the FRL domain for this test's CREATE DATABASE call below.
+ RelationalKeyspaceProvider.instance().registerDomainIfNotExists("FRL");
+ // Catalog mutations + selects from system tables, all serialised + retried via
+ // CatalogOperations so we don't race other test classes' /__SYS work.
+ // Not all of this is DDL, but this is easier, and not worth trying to
+ // better parallelize our tests
+ CatalogOperations.runOnCatalog(getDriver(), connection -> {
try (Statement statement = connection.createStatement()) {
// Make this better... currently returns zero how ever many rows we touch.
Assertions.assertEquals(0, statement.executeUpdate("Drop database if exists \"" + TESTDB + "\""));
@@ -183,7 +133,7 @@ public void simpleStatement() throws SQLException {
statement.executeUpdate("Drop schema template if exists test_template");
}
}
- }
+ });
}
private void checkSelectStarFromDatabasesResultSet(ResultSet resultSet) throws SQLException {
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/AutoCommitTests.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/AutoCommitTests.java
index f3ab88316d..bb8d1b46f0 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/AutoCommitTests.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/AutoCommitTests.java
@@ -54,11 +54,11 @@ public class AutoCommitTests {
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(AutoCommitTests.class, TestSchemas.restaurant());
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, AutoCommitTests.class, TestSchemas.restaurant());
@RegisterExtension
@Order(2)
- public final RelationalConnectionRule connection = new RelationalConnectionRule(database::getConnectionUri)
+ public final RelationalConnectionRule connection = new RelationalConnectionRule(relationalExtension, database::getConnectionUri)
.withOptions(Options.NONE)
.withSchema("TEST_SCHEMA");
@@ -461,7 +461,7 @@ public void catalogMetadata(TransactionType transactionType) throws SQLException
conn = getConnectionWithExistingTransaction(conn, database.getConnectionUri(), alternateDriver);
}
setAutoCommit(conn, transactionType);
- try (final var rs = conn.getMetaData().getTables("/TEST/AutoCommitTests", "TEST_SCHEMA", null, null)) {
+ try (final var rs = conn.getMetaData().getTables(database.getDatabasePath().getPath(), "TEST_SCHEMA", null, null)) {
ResultSetAssert.assertThat(rs)
.hasNextRow()
.hasNextRow()
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/BasicMetadataTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/BasicMetadataTest.java
index 3f67c03c99..a5e151947e 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/BasicMetadataTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/BasicMetadataTest.java
@@ -54,12 +54,12 @@ public class BasicMetadataTest {
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension,
BasicMetadataTest.class, TestSchemas.restaurant());
@RegisterExtension
@Order(2)
- public final RelationalConnectionRule dbConn = new RelationalConnectionRule(database::getConnectionUri);
+ public final RelationalConnectionRule dbConn = new RelationalConnectionRule(relationalExtension, database::getConnectionUri);
@Test
void canGetPrimaryKeysForTable() throws SQLException {
@@ -162,10 +162,10 @@ void canGetTableColumns() throws SQLException {
void canGetTableIndexes() throws SQLException {
final RelationalDatabaseMetaData metaData = dbConn.getMetaData();
Assertions.assertNotNull(metaData, "Null metadata returned");
- try (final RelationalResultSet tableData = metaData.getIndexInfo("/TEST/BasicMetadataTest", "TEST_SCHEMA", "RESTAURANT_REVIEWER", false, false)) {
+ try (final RelationalResultSet tableData = metaData.getIndexInfo(database.getDatabasePath().getPath(), "TEST_SCHEMA", "RESTAURANT_REVIEWER", false, false)) {
ResultSetAssert.assertThat(tableData).hasNextRow()
.isRowExactly(
- "/TEST/BasicMetadataTest", //table_cat
+ database.getDatabasePath().getPath(), //table_cat
"TEST_SCHEMA", //table_schem
"RESTAURANT_REVIEWER", //table_name
false, //non_unique
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/CaseSensitiveDbObjectsTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/CaseSensitiveDbObjectsTest.java
index 5e0ea17305..1ca8df5cee 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/CaseSensitiveDbObjectsTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/CaseSensitiveDbObjectsTest.java
@@ -35,14 +35,22 @@
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.parallel.Isolated;
import javax.annotation.Nonnull;
-import java.sql.DriverManager;
import java.sql.SQLException;
+import java.net.URI;
/**
* Test case-sensitive db object connection option.
*/
+// Marked @Isolated because this test asserts on captured log messages from the JVM-global
+// PlanGenerator logger via LogAppenderRule. The appender catches events from any test
+// running concurrently against the same logger, and a thread-id filter is not safe
+// because the relational engine dispatches work onto async pools (FDB callbacks,
+// CompletableFuture stages, etc.). @Isolated tells JUnit to suspend all other tests
+// while this class runs, so the captured events are guaranteed to be ours.
+@Isolated
public class CaseSensitiveDbObjectsTest {
@Nonnull
private static final String SCHEMA_TEMPLATE =
@@ -56,13 +64,13 @@ public class CaseSensitiveDbObjectsTest {
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(CaseSensitiveDbObjectsTest.class,
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, CaseSensitiveDbObjectsTest.class,
SCHEMA_TEMPLATE, Options.builder().withOption(Options.Name.CASE_SENSITIVE_IDENTIFIERS, true).build(),
new SchemaTemplateRule.SchemaTemplateOptions(true, true));
@RegisterExtension
@Order(2)
- public final RelationalConnectionRule connection = new RelationalConnectionRule(database::getConnectionUri)
+ public final RelationalConnectionRule connection = new RelationalConnectionRule(relationalExtension, database::getConnectionUri)
.withOptions(Options.builder().withOption(Options.Name.CASE_SENSITIVE_IDENTIFIERS, true).build())
.withSchema("TEST_SCHEMA");
@@ -153,8 +161,8 @@ void planIsProperlyCachedAndReusedAcrossCaseOptionVariation() throws SQLExceptio
Assertions.assertThat(logAppender.getLastLogEventMessage()).contains("select 'id' from 't1' where 'group' = ?");
Assertions.assertThat(logAppender.getLastLogEventMessage()).contains("planCache=\"miss\"");
- try (RelationalConnection caseInsensitiveConn = DriverManager.getConnection(database.getConnectionUri()
- .toString()).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection caseInsensitiveConn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri()
+ .toString())).unwrap(RelationalConnection.class)) {
caseInsensitiveConn.setSchema("TEST_SCHEMA");
try (RelationalStatement caseInsensitiveStatement = caseInsensitiveConn.createStatement()) {
try (RelationalResultSet resultSet = caseInsensitiveStatement.executeQuery(
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/CaseSensitivityTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/CaseSensitivityTest.java
index 918b6bb167..ed9b79e0d5 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/CaseSensitivityTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/CaseSensitivityTest.java
@@ -28,6 +28,7 @@
import com.apple.foundationdb.relational.api.RelationalResultSet;
import com.apple.foundationdb.relational.api.RelationalStatement;
import com.apple.foundationdb.relational.api.exceptions.ErrorCode;
+import com.apple.foundationdb.relational.utils.CatalogOperations;
import com.apple.foundationdb.relational.utils.Ddl;
import com.apple.foundationdb.relational.utils.ResultSetAssert;
import com.apple.foundationdb.relational.utils.RelationalAssertions;
@@ -41,7 +42,6 @@
import org.junit.jupiter.params.provider.ValueSource;
import java.net.URI;
-import java.sql.DriverManager;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
@@ -62,7 +62,7 @@ public CaseSensitivityTest() {
@Test
void selectFromCaseInsensitiveTable() throws Exception {
final String schema = "CREATE TABLE tbl1 (id bigint, value bigint, PRIMARY KEY(id))";
- try (var ddl = Ddl.builder().database(URI.create("/TEST/CaseSensitivity")).relationalExtension(relationalExtension).schemaTemplate(schema).build()) {
+ try (var ddl = Ddl.builder().database().relationalExtension(relationalExtension).schemaTemplate(schema).build()) {
try (var statement = ddl.setSchemaAndGetConnection().createStatement()) {
for (String tableName : List.of("tbl1", "TBL1", "TbL1", "\"TBL1\"")) {
Assertions.assertDoesNotThrow(() -> statement.execute("SELECT * FROM " + tableName));
@@ -77,8 +77,7 @@ void selectFromCaseInsensitiveTable() throws Exception {
@Test
public void databaseWithSameUpperName() throws Exception {
- try (final var conn = DriverManager.getConnection("jdbc:embed:/__SYS")) {
- conn.setSchema("CATALOG");
+ CatalogOperations.runOnCatalog(relationalExtension.getDriver(), conn -> {
try (final var statement = conn.createStatement()) {
//create a database
statement.executeUpdate("CREATE DATABASE /test/upper");
@@ -91,7 +90,8 @@ public void databaseWithSameUpperName() throws Exception {
statement.executeUpdate("DROP DATABASE /test/upper");
}
}
- }
+ });
+
}
private String quote(String dbObject, boolean quote) {
@@ -106,8 +106,7 @@ private String quote(String dbObject, boolean quote) {
@ValueSource(booleans = {true, false})
public void variousDatabases(boolean quoted) throws Exception {
List databases = List.of("/TEST/ABC1", "/TEST/def2", "/TEST/Ghi3", "/TEST/jKL4");
- try (final var conn = DriverManager.getConnection("jdbc:embed:/__SYS")) {
- conn.setSchema("CATALOG");
+ CatalogOperations.runOnCatalog(relationalExtension.getDriver(), conn -> {
try {
try (final var statement = conn.createStatement()) {
// Quoted databases
@@ -135,15 +134,15 @@ public void variousDatabases(boolean quoted) throws Exception {
}
}
}
- }
+ });
+
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void variousSchemas(boolean quoted) throws Exception {
List schemas = List.of("ABC2", "def3", "Ghi4", "jKL5");
- try (final var conn = DriverManager.getConnection("jdbc:embed:/__SYS")) {
- conn.setSchema("CATALOG");
+ CatalogOperations.runOnCatalog(relationalExtension.getDriver(), conn -> {
try {
try (final var statement = conn.createStatement()) {
statement.executeUpdate("DROP DATABASE if exists /test/various_schemas");
@@ -168,7 +167,8 @@ public void variousSchemas(boolean quoted) throws Exception {
statement.executeUpdate("DROP SCHEMA TEMPLATE temp_various_schemas");
}
}
- }
+ });
+
}
@ParameterizedTest
@@ -179,8 +179,8 @@ public void variousSchemas(boolean quoted) throws Exception {
"SchemaTemplateCatalog))")
public void variousStructs(boolean quoted) throws Exception {
List structs = List.of(/*"ABC1",*/ "def2", "Ghi3", "jKL4");
- try (RelationalConnection conn = DriverManager.getConnection("jdbc:embed:/__SYS").unwrap(RelationalConnection.class)) {
- conn.setSchema("CATALOG");
+ CatalogOperations.runOnCatalog(relationalExtension.getDriver(), connection -> {
+ RelationalConnection conn = connection.unwrap(RelationalConnection.class);
try {
try (RelationalStatement statement = conn.createStatement()) {
for (String struct : structs) {
@@ -197,15 +197,16 @@ public void variousStructs(boolean quoted) throws Exception {
}
}
}
- }
+ });
+
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void variousTables(boolean quoted) throws Exception {
List tables = List.of("ABC1", "def2", "Ghi3", "jKL4");
- try (RelationalConnection conn = DriverManager.getConnection("jdbc:embed:/__SYS").unwrap(RelationalConnection.class)) {
- conn.setSchema("CATALOG");
+ CatalogOperations.runOnCatalog(relationalExtension.getDriver(), connection -> {
+ RelationalConnection conn = connection.unwrap(RelationalConnection.class);
try {
try (RelationalStatement statement = conn.createStatement()) {
statement.executeUpdate("DROP DATABASE if exists /test/various_tables_db");
@@ -238,15 +239,16 @@ public void variousTables(boolean quoted) throws Exception {
}
}
}
- }
+ });
+
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void variousColumns(boolean quoted) throws Exception {
List columns = List.of("ABC1", "def2", "Ghi3", "jKL4");
- try (RelationalConnection conn = DriverManager.getConnection("jdbc:embed:/__SYS").unwrap(RelationalConnection.class)) {
- conn.setSchema("CATALOG");
+ CatalogOperations.runOnCatalog(relationalExtension.getDriver(), connection -> {
+ RelationalConnection conn = connection.unwrap(RelationalConnection.class);
try {
try (RelationalStatement statement = conn.createStatement()) {
statement.executeUpdate("CREATE DATABASE /test/various_columns_db");
@@ -277,7 +279,8 @@ public void variousColumns(boolean quoted) throws Exception {
}
}
}
- }
+ });
+
}
@Test
@@ -286,8 +289,8 @@ public void overload() throws Exception {
List schemas = List.of("schema", "SCHEMA", "Schema", "ScHeMa");
List tables = List.of("table", "TABLE", "Table", "TaBlE");
List columns = List.of("column", "COLUMN", "Column", "CoLuMn");
- try (RelationalConnection conn = DriverManager.getConnection("jdbc:embed:/__SYS").unwrap(RelationalConnection.class)) {
- conn.setSchema("CATALOG");
+ CatalogOperations.runOnCatalog(relationalExtension.getDriver(), connection -> {
+ RelationalConnection conn = connection.unwrap(RelationalConnection.class);
try {
try (RelationalStatement statement = conn.createStatement()) {
for (String schema : schemas) {
@@ -315,7 +318,7 @@ public void overload() throws Exception {
}
long value = 0;
// Data insertion
- try (RelationalConnection dbConn = DriverManager.getConnection("jdbc:embed:" + database).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection dbConn = relationalExtension.getDriver().connect(URI.create("jdbc:embed:" + database)).unwrap(RelationalConnection.class)) {
try (RelationalStatement statement = dbConn.createStatement()) {
for (String schema : schemas) {
dbConn.setSchema(schema);
@@ -331,7 +334,7 @@ public void overload() throws Exception {
}
value = 0;
// Data retrieval
- try (RelationalConnection dbConn = DriverManager.getConnection("jdbc:embed:" + database).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection dbConn = relationalExtension.getDriver().connect(URI.create("jdbc:embed:" + database)).unwrap(RelationalConnection.class)) {
try (RelationalStatement statement = dbConn.createStatement()) {
for (String schema : schemas) {
dbConn.setSchema(schema);
@@ -357,6 +360,7 @@ public void overload() throws Exception {
}
}
}
- }
+ });
+
}
}
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/CursorTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/CursorTest.java
index b4968acdda..8af55bfc88 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/CursorTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/CursorTest.java
@@ -26,7 +26,6 @@
import com.apple.foundationdb.relational.api.Row;
import com.apple.foundationdb.relational.api.StructMetaData;
import com.apple.foundationdb.relational.api.RelationalConnection;
-import com.apple.foundationdb.relational.api.RelationalDriver;
import com.apple.foundationdb.relational.api.RelationalResultSet;
import com.apple.foundationdb.relational.api.RelationalStatement;
import com.apple.foundationdb.relational.api.RelationalStruct;
@@ -41,11 +40,11 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
-import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
+import java.net.URI;
public class CursorTest {
@RegisterExtension
@@ -54,7 +53,7 @@ public class CursorTest {
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension,
CursorTest.class,
TestSchemas.restaurant());
@@ -199,7 +198,7 @@ public void continuationWithScanRowLimit() throws SQLException, RelationalExcept
Continuation continuation;
int numRowsReturned = 0;
// 1. Iterate over and count the rows returned before the scan rows limit is hit
- final var driver = (RelationalDriver) DriverManager.getDriver(database.getConnectionUri().toString());
+ final var driver = relationalExtension.getDriver();
try (final var conn = driver.connect(database.getConnectionUri(), Options.builder().withOption(Options.Name.EXECUTION_SCANNED_ROWS_LIMIT, 3).build())) {
conn.setSchema(database.getSchemaName());
try (final var resultSet = conn.createStatement().executeQuery("select * from RESTAURANT")) {
@@ -216,7 +215,7 @@ public void continuationWithScanRowLimit() throws SQLException, RelationalExcept
}
}
// 2. Further count the rows in other execution without limits and see if total number of rows is 10
- try (final var conn = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (final var conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
conn.setSchema(database.getSchemaName());
try (final var preparedStatement = conn.prepareStatement("EXECUTE CONTINUATION ?param")) {
preparedStatement.setBytes("param", continuation.serialize());
@@ -241,7 +240,7 @@ public void continuationWithScanRowLimit() throws SQLException, RelationalExcept
private void insertRecordsAndTest(int numRecords,
BiConsumer, RelationalConnection> test) throws SQLException, RelationalException {
final var records = insertAndReturnRecords(numRecords);
- try (final var con = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (final var con = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
con.setSchema(database.getSchemaName());
test.accept(records, con);
}
@@ -249,7 +248,7 @@ private void insertRecordsAndTest(int numRecords,
private List insertAndReturnRecords(int numRecords) throws SQLException {
List records;
- try (final var con = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (final var con = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
con.setSchema(database.getSchemaName());
final var statement = con.createStatement();
records = Utils.generateRestaurantRecords(numRecords);
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeNoMetadataKeyTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeNoMetadataKeyTest.java
index f05c2d255c..51c1e79f05 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeNoMetadataKeyTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeNoMetadataKeyTest.java
@@ -38,7 +38,6 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
-import java.net.URI;
import java.sql.SQLException;
import java.util.List;
@@ -59,11 +58,11 @@ CREATE TABLE t2 (id bigint, a string, b string, e bigint, f boolean, PRIMARY KEY
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(DeleteRangeNoMetadataKeyTest.class, SCHEMA_TEMPLATE, new SchemaTemplateRule.SchemaTemplateOptions(true, true));
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, DeleteRangeNoMetadataKeyTest.class, SCHEMA_TEMPLATE, new SchemaTemplateRule.SchemaTemplateOptions(true, true));
@RegisterExtension
@Order(2)
- public final RelationalConnectionRule connection = new RelationalConnectionRule(database::getConnectionUri)
+ public final RelationalConnectionRule connection = new RelationalConnectionRule(relationalExtension, database::getConnectionUri)
.withOptions(Options.NONE)
.withSchema("TEST_SCHEMA");
@@ -397,7 +396,7 @@ void testDeleteWithIndexSamePrefixButDeleteGoesBeyondIndex() throws Exception {
private Ddl getDdl(String templateSuffix) throws Exception {
return Ddl.builder()
- .database(URI.create("/TEST/QT"))
+ .database()
.relationalExtension(relationalExtension)
.schemaTemplate(SCHEMA_TEMPLATE + templateSuffix)
.schemaTemplateOptions(new SchemaTemplateRule.SchemaTemplateOptions(true, true))
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeTest.java
index 29f8933c3d..193224bd05 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeTest.java
@@ -37,7 +37,6 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
-import java.net.URI;
import java.util.List;
/**
@@ -52,11 +51,11 @@ public class DeleteRangeTest {
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(DeleteRangeTest.class, SCHEMA_TEMPLATE);
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, DeleteRangeTest.class, SCHEMA_TEMPLATE);
@RegisterExtension
@Order(2)
- public final RelationalConnectionRule connection = new RelationalConnectionRule(database::getConnectionUri)
+ public final RelationalConnectionRule connection = new RelationalConnectionRule(relationalExtension, database::getConnectionUri)
.withOptions(Options.NONE)
.withSchema("TEST_SCHEMA");
@@ -221,7 +220,7 @@ void deleteUsingNonKeyColumns() throws Exception {
@Test
void testDeleteWithIndexWithSamePrefix() throws Exception {
final String schemaTemplate = SCHEMA_TEMPLATE + " CREATE INDEX idx1 ON t1(id, a)";
- try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) {
+ try (var ddl = Ddl.builder().database().relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) {
try (var stmt = ddl.setSchemaAndGetConnection().createStatement()) {
insertData(stmt);
KeySet toDelete = new KeySet()
@@ -251,7 +250,7 @@ void testDeleteWithIndexWithSamePrefix() throws Exception {
@Test
void testDeleteWithIndexSamePrefixButDeleteGoesBeyondIndex() throws Exception {
final String schemaTemplate = SCHEMA_TEMPLATE + " CREATE INDEX idx1 ON t1(id)";
- try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) {
+ try (var ddl = Ddl.builder().database().relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) {
try (var stmt = ddl.setSchemaAndGetConnection().createStatement()) {
insertData(stmt);
KeySet toDelete = new KeySet()
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalExtension.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalExtension.java
index 7944579ff0..bd382294b9 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalExtension.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalExtension.java
@@ -35,6 +35,7 @@
import com.apple.foundationdb.relational.recordlayer.catalog.StoreCatalogProvider;
import com.apple.foundationdb.relational.recordlayer.ddl.RecordLayerMetadataOperationsFactory;
import com.apple.foundationdb.relational.recordlayer.query.cache.RelationalPlanCache;
+import com.apple.foundationdb.relational.utils.CatalogOperations;
import com.apple.foundationdb.test.FDBTestEnvironment;
import com.codahale.metrics.MetricRegistry;
import org.junit.jupiter.api.extension.AfterEachCallback;
@@ -44,7 +45,6 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Closeable;
-import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Collections;
@@ -67,34 +67,31 @@ public class EmbeddedRelationalExtension implements RelationalExtension, BeforeE
private StoreCatalog storeCatalog;
private FDBDatabase database;
private final String clusterFile;
- private final boolean register;
public EmbeddedRelationalExtension() {
this(Options.none());
}
public EmbeddedRelationalExtension(@Nonnull final Options options) {
- this(FDBTestEnvironment.randomClusterFile(), true, options);
+ this(FDBTestEnvironment.randomClusterFile(), options);
}
- public EmbeddedRelationalExtension(final String clusterFile, final boolean register, final Options options) {
+ public EmbeddedRelationalExtension(final String clusterFile, final Options options) {
final RelationalKeyspaceProvider keyspaceProvider = RelationalKeyspaceProvider.instance();
keyspaceProvider.registerDomainIfNotExists("TEST");
this.keySpace = keyspaceProvider.getKeySpace();
this.storeTimer = new MetricRegistry();
this.clusterFile = clusterFile;
- this.register = register;
this.options = options;
}
@Override
public void afterEach(ExtensionContext ignored) throws Exception {
- if (driver != null) {
- if (register) {
- DriverManager.deregisterDriver(driver);
- }
- driver = null;
- }
+ // Drop the per-instance driver reference. Nothing to do with java.sql.DriverManager —
+ // the extension never registers its driver there. Tests should use this extension's
+ // {@link #getDriver()} (via field injection into the rules and {@code Ddl}), not
+ // {@code DriverManager.getConnection}.
+ driver = null;
}
@Override
@@ -115,9 +112,6 @@ private void setup() throws RelationalException, SQLException {
// most likely touch the catalog, and could affect mixed-mode tests
engine = makeEngine(database, FormatVersion.getDefaultFormatVersion());
driver = new EmbeddedRelationalDriver(engine);
- if (register) {
- DriverManager.registerDriver(driver);
- }
}
public EmbeddedRelationalDriver getDriver(@Nonnull final FormatVersion formatVersion) throws SQLException {
@@ -126,11 +120,26 @@ public EmbeddedRelationalDriver getDriver(@Nonnull final FormatVersion formatVer
private void makeDatabase(String clusterFile) throws RelationalException {
database = FDBDatabaseFactory.instance().getDatabase(clusterFile);
- try (var connection = new DirectFdbConnection(database);
- Transaction txn = connection.getTransactionManager().createTransaction(Options.NONE)) {
- storeCatalog = StoreCatalogProvider.getCatalog(txn, keySpace);
- txn.commit();
- }
+ // The catalog-bootstrap transaction below writes the cluster-wide catalog metadata, which
+ // races with every other test class's @BeforeEach. Without the JVM-wide lock, parallel
+ // class execution surfaces these races as `FDBStoreTransactionConflictException`. See
+ // CatalogOperations for the lock contract.
+ //
+ // Deliberately NOT try-with-resources on the DirectFdbConnection — its close() calls
+ // fdb.close() on the shared FDBDatabaseFactory handle that every other test in this
+ // JVM is holding. Closing it here (once per @BeforeEach) invalidates the native pointer
+ // and any sibling test with an in-flight transaction fails with
+ // `IllegalStateException: Cannot access closed object` from NativeObjectWrapper.getPtr.
+ // We only need the Transaction closed; the connection is a lightweight wrapper we can
+ // leak.
+ // Probably DirectFdbConnection should not close the underlying database
+ CatalogOperations.runLockedWithRelationalRetry(() -> {
+ final var connection = new DirectFdbConnection(database);
+ try (Transaction txn = connection.getTransactionManager().createTransaction(Options.NONE)) {
+ storeCatalog = StoreCatalogProvider.getCatalog(txn, keySpace);
+ txn.commit();
+ }
+ });
}
private EmbeddedRelationalEngine makeEngine(final @Nonnull FDBDatabase database, final @Nonnull FormatVersion formatVersion) {
@@ -181,7 +190,7 @@ public EmbeddedRelationalExtension extensionForOtherCluster() throws SQLExceptio
final String otherClusterFile = FDBTestEnvironment.allClusterFiles().stream()
.filter(clusterFile -> !clusterFile.equals(database.getClusterFile()))
.findAny().orElseThrow();
- final EmbeddedRelationalExtension embeddedRelationalExtension = new EmbeddedRelationalExtension(otherClusterFile, false, options);
+ final EmbeddedRelationalExtension embeddedRelationalExtension = new EmbeddedRelationalExtension(otherClusterFile, options);
embeddedRelationalExtension.setup();
return embeddedRelationalExtension;
}
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/InsertTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/InsertTest.java
index ec27af321d..c7462a8209 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/InsertTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/InsertTest.java
@@ -41,10 +41,10 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
-import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
+import java.net.URI;
public class InsertTest {
@RegisterExtension
@@ -53,14 +53,14 @@ public class InsertTest {
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(InsertTest.class, TestSchemas.restaurant());
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, InsertTest.class, TestSchemas.restaurant());
@Test
void canInsertWithMultipleRecordTypes() throws SQLException {
/*
* We want to make sure that we don't accidentally pick up data from different tables
*/
- try (RelationalConnection conn = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
conn.setSchema("TEST_SCHEMA");
try (RelationalStatement s = conn.createStatement()) {
long id = System.currentTimeMillis();
@@ -150,7 +150,7 @@ void cannotInsertWithIncorrectTypeForRecord() throws SQLException {
/*
* We want to make sure that we don't accidentally pick up data from different tables
*/
- try (RelationalConnection conn = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
conn.setSchema("TEST_SCHEMA");
try (RelationalStatement s = conn.createStatement()) {
long id = System.currentTimeMillis();
@@ -164,7 +164,7 @@ final var record = EmbeddedRelationalStruct.newBuilder().addLong("REST_NO", id).
@Test
void canDifferentiateNullAndDefaultValue() throws SQLException {
- try (RelationalConnection conn = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
conn.setSchema("TEST_SCHEMA");
try (RelationalStatement s = conn.createStatement()) {
long id = System.currentTimeMillis();
@@ -199,7 +199,7 @@ void canDifferentiateNullAndDefaultValue() throws SQLException {
@Test
void cannotInsertDuplicatePrimaryKey() throws SQLException {
- try (RelationalConnection conn = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
conn.setSchema("TEST_SCHEMA");
try (RelationalStatement s = conn.createStatement()) {
final var struct = EmbeddedRelationalStruct.newBuilder().addLong("REST_NO", 0).build();
@@ -224,7 +224,7 @@ void canInsertNullableArray() throws SQLException {
DataType.StructType.Field.from("PRICE", DataType.Primitives.NULLABLE_FLOAT.type(), 1)
), false), false);
- try (RelationalConnection conn = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
conn.setSchema("TEST_SCHEMA");
try (RelationalStatement s = conn.createStatement()) {
@@ -282,7 +282,7 @@ void canInsertNullableArray() throws SQLException {
@Test
void replaceOnInsert() throws SQLException {
- try (RelationalConnection conn = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
conn.setSchema("TEST_SCHEMA");
try (RelationalStatement s = conn.createStatement()) {
RelationalStruct record = EmbeddedRelationalStruct.newBuilder()
@@ -310,7 +310,7 @@ record = EmbeddedRelationalStruct.newBuilder()
@Test
void replaceOnFirstInsert() throws SQLException {
- try (RelationalConnection conn = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
+ try (RelationalConnection conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString())).unwrap(RelationalConnection.class)) {
conn.setSchema("TEST_SCHEMA");
try (RelationalStatement s = conn.createStatement()) {
RelationalStruct record = EmbeddedRelationalStruct.newBuilder().addLong("REST_NO", 0).build();
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/IntermingledTablesTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/IntermingledTablesTest.java
index 15f01e5922..104d77691c 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/IntermingledTablesTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/IntermingledTablesTest.java
@@ -55,11 +55,11 @@ CREATE TABLE t2 (group bigint, id string, val2 string, PRIMARY KEY(group, id))
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(IntermingledTablesTest.class, SCHEMA_TEMPLATE, new SchemaTemplateRule.SchemaTemplateOptions(true, true));
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, IntermingledTablesTest.class, SCHEMA_TEMPLATE, new SchemaTemplateRule.SchemaTemplateOptions(true, true));
@RegisterExtension
@Order(2)
- public final RelationalConnectionRule connection = new RelationalConnectionRule(database::getConnectionUri)
+ public final RelationalConnectionRule connection = new RelationalConnectionRule(relationalExtension, database::getConnectionUri)
.withOptions(Options.NONE)
.withSchema("TEST_SCHEMA");
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/JoinWithLimitTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/JoinWithLimitTest.java
index 7849a88605..7878f9601d 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/JoinWithLimitTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/JoinWithLimitTest.java
@@ -49,11 +49,11 @@ CREATE TABLE Q(qpk bigint, d D array, PRIMARY KEY(qpk))
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule db = new SimpleDatabaseRule(UniqueIndexTests.class, getTemplate_definition);
+ public final SimpleDatabaseRule db = new SimpleDatabaseRule(relationalExtension, UniqueIndexTests.class, getTemplate_definition);
@RegisterExtension
@Order(2)
- public final RelationalConnectionRule connection = new RelationalConnectionRule(db::getConnectionUri)
+ public final RelationalConnectionRule connection = new RelationalConnectionRule(relationalExtension, db::getConnectionUri)
.withSchema(db.getSchemaName());
@RegisterExtension
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/LogAppenderRule.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/LogAppenderRule.java
index 3270134982..72729750b1 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/LogAppenderRule.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/LogAppenderRule.java
@@ -36,6 +36,16 @@
import java.util.ArrayList;
import java.util.List;
+/**
+ * Test rule that captures Log4j events emitted on a target logger for the duration of a test.
+ *
+ * The appender is attached to a JVM-global logger, so the rule cannot meaningfully separate its
+ * own events from events emitted by concurrently-running sibling tests — and a naive thread-id
+ * filter doesn't help either, because the relational engine dispatches work onto async pools
+ * (FDB callbacks, CompletableFuture stages, etc.). Test classes that use this rule and assert
+ * on captured log content should therefore mark themselves {@code @Isolated} so JUnit suspends
+ * all other tests while they run. See {@code QueryLoggingTest} for an example.
+ */
public class LogAppenderRule implements BeforeEachCallback, AfterEachCallback, AutoCloseable {
private LogAppender logAppender;
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/NullsInResultSetTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/NullsInResultSetTest.java
index 557b85efcd..1d8e24bd91 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/NullsInResultSetTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/NullsInResultSetTest.java
@@ -31,7 +31,6 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
-import java.net.URI;
import java.nio.charset.StandardCharsets;
public class NullsInResultSetTest {
@@ -50,7 +49,7 @@ public NullsInResultSetTest() {
@Test
void nullValues() throws Exception {
- try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) {
+ try (var ddl = Ddl.builder().database().relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) {
try (RelationalConnection conn = ddl.setSchemaAndGetConnection()) {
try (RelationalStatement s = conn.createStatement()) {
s.execute("INSERT INTO T(PK) VALUES(100)");
@@ -93,7 +92,7 @@ void nullValues() throws Exception {
@Test
void notNullValues() throws Exception {
- try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) {
+ try (var ddl = Ddl.builder().database().relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) {
try (RelationalConnection conn = ddl.setSchemaAndGetConnection()) {
try (RelationalStatement s = conn.createStatement()) {
final var struct = EmbeddedRelationalStruct.newBuilder()
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/OfflinePlanGenerationTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/OfflinePlanGenerationTest.java
index d3d93166fa..3578de699b 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/OfflinePlanGenerationTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/OfflinePlanGenerationTest.java
@@ -58,12 +58,12 @@ class OfflinePlanGenerationTest {
@RegisterExtension
@Order(2)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension,
OfflinePlanGenerationTest.class, TestSchemas.restaurant());
@RegisterExtension
@Order(3)
- public final RelationalConnectionRule connection = new RelationalConnectionRule(database::getConnectionUri)
+ public final RelationalConnectionRule connection = new RelationalConnectionRule(relationalExtension, database::getConnectionUri)
.withSchema("TEST_SCHEMA");
@RegisterExtension
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/OptionScopeTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/OptionScopeTest.java
index ce8c92d5ae..efaf108712 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/OptionScopeTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/OptionScopeTest.java
@@ -21,7 +21,6 @@
package com.apple.foundationdb.relational.recordlayer;
import com.apple.foundationdb.relational.api.Options;
-import com.apple.foundationdb.relational.api.RelationalDriver;
import com.apple.foundationdb.relational.api.RelationalResultSet;
import com.apple.foundationdb.relational.api.exceptions.RelationalException;
import com.apple.foundationdb.relational.utils.ResultSetAssert;
@@ -34,10 +33,10 @@
import org.junit.jupiter.api.extension.RegisterExtension;
import java.sql.Connection;
-import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
+import java.net.URI;
public class OptionScopeTest {
@@ -51,11 +50,11 @@ public class OptionScopeTest {
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule db = new SimpleDatabaseRule(UniqueIndexTests.class, TestSchemas.books());
+ public final SimpleDatabaseRule db = new SimpleDatabaseRule(relationalExtension, UniqueIndexTests.class, TestSchemas.books());
@Test
public void optionTakenFromConnection() throws SQLException, RelationalException {
- final var driver = (RelationalDriver) DriverManager.getDriver(db.getConnectionUri().toString());
+ final var driver = relationalExtension.getDriver();
try (Connection conn = driver.connect(db.getConnectionUri(), Options.builder().withOption(Options.Name.DRY_RUN, true).build())) {
conn.setSchema(db.getSchemaName());
try (Statement statement = conn.createStatement()) {
@@ -69,7 +68,7 @@ public void optionTakenFromConnection() throws SQLException, RelationalException
@Test
public void optionTakenFromQuery() throws SQLException {
- try (Connection conn = DriverManager.getConnection(db.getConnectionUri().toString())) {
+ try (Connection conn = relationalExtension.getDriver().connect(URI.create(db.getConnectionUri().toString()))) {
conn.setSchema(db.getSchemaName());
try (Statement statement = conn.createStatement()) {
Assertions.assertThat(statement.executeUpdate(INSERT_QUERY_DRY_RUN)).isOne();
@@ -82,7 +81,7 @@ public void optionTakenFromQuery() throws SQLException {
@Test
public void optionSetInConnectionButOverriddenInQuery() throws SQLException, RelationalException {
- final var driver = (RelationalDriver) DriverManager.getDriver(db.getConnectionUri().toString());
+ final var driver = relationalExtension.getDriver();
try (Connection conn = driver.connect(db.getConnectionUri(), Options.builder().withOption(Options.Name.DRY_RUN, false).build())) {
conn.setSchema(db.getSchemaName());
try (Statement statement = conn.createStatement()) {
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/PlanGenerationStackTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/PlanGenerationStackTest.java
index ac08ab221d..1eec5c9055 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/PlanGenerationStackTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/PlanGenerationStackTest.java
@@ -63,11 +63,11 @@ public class PlanGenerationStackTest {
@RegisterExtension
@Order(2)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(PlanGenerationStackTest.class, TestSchemas.restaurant());
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, PlanGenerationStackTest.class, TestSchemas.restaurant());
@RegisterExtension
@Order(3)
- public final RelationalConnectionRule connection = new RelationalConnectionRule(database::getConnectionUri)
+ public final RelationalConnectionRule connection = new RelationalConnectionRule(relationalExtension, database::getConnectionUri)
.withSchema("TEST_SCHEMA");
@RegisterExtension
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/QueryLoggingTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/QueryLoggingTest.java
index b10eb913b4..71d693326f 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/QueryLoggingTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/QueryLoggingTest.java
@@ -25,7 +25,6 @@
import com.apple.foundationdb.relational.api.Continuation;
import com.apple.foundationdb.relational.api.Options;
import com.apple.foundationdb.relational.api.RelationalConnection;
-import com.apple.foundationdb.relational.api.RelationalDriver;
import com.apple.foundationdb.relational.api.RelationalPreparedStatement;
import com.apple.foundationdb.relational.api.RelationalResultSet;
import com.apple.foundationdb.relational.recordlayer.query.AstNormalizer;
@@ -39,10 +38,10 @@
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.parallel.Isolated;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
-import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -52,6 +51,13 @@
/**
* Testing basic query logging: plan, time, cache hits, etc.
*/
+// Marked @Isolated because this test asserts on captured log messages from the JVM-global
+// PlanGenerator logger via LogAppenderRule. The appender catches events from any test
+// running concurrently against the same logger, and a thread-id filter is not safe
+// because the relational engine dispatches work onto async pools (FDB callbacks,
+// CompletableFuture stages, etc.). @Isolated tells JUnit to suspend all other tests
+// while this class runs, so the captured events are guaranteed to be ours.
+@Isolated
public class QueryLoggingTest {
@RegisterExtension
@Order(0)
@@ -59,11 +65,11 @@ public class QueryLoggingTest {
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(QueryLoggingTest.class, TestSchemas.restaurantWithCoveringIndex());
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, QueryLoggingTest.class, TestSchemas.restaurantWithCoveringIndex());
@RegisterExtension
@Order(2)
- public final RelationalConnectionRule connection = new RelationalConnectionRule(database::getConnectionUri)
+ public final RelationalConnectionRule connection = new RelationalConnectionRule(relationalExtension, database::getConnectionUri)
.withOptions(Options.NONE)
.withSchema("TEST_SCHEMA");
@@ -133,7 +139,7 @@ void testNoLogIfMissing() throws Exception {
@Test
void testRelationalConnectionOptionPreparedStatement() throws Exception {
- final var driver = (RelationalDriver) DriverManager.getDriver(database.getConnectionUri().toString());
+ final var driver = relationalExtension.getDriver();
try (RelationalConnection conn = driver.connect(database.getConnectionUri(), Options.builder().withOption(Options.Name.LOG_QUERY, true).build())) {
conn.setSchema(database.getSchemaName());
try (PreparedStatement ps = conn.prepareStatement("SELECT name from restaurant where rest_no = ?")) {
@@ -154,7 +160,7 @@ void testRelationalConnectionOptionPreparedStatement() throws Exception {
@Test
void testRelationalConnectionOptionExplicitlyDisabled() throws Exception {
- final var driver = (RelationalDriver) DriverManager.getDriver(database.getConnectionUri().toString());
+ final var driver = relationalExtension.getDriver();
try (RelationalConnection conn = driver.connect(database.getConnectionUri(), Options.builder().withOption(Options.Name.LOG_QUERY, false).build())) {
conn.setSchema(database.getSchemaName());
try (PreparedStatement ps = conn.prepareStatement("SELECT name from restaurant where rest_no = ?")) {
@@ -175,7 +181,7 @@ void testRelationalConnectionOptionExplicitlyDisabled() throws Exception {
@Test
void testRelationalConnectionSetLogOnThenOff() throws Exception {
- final var driver = (RelationalDriver) DriverManager.getDriver(database.getConnectionUri().toString());
+ final var driver = relationalExtension.getDriver();
try (RelationalConnection conn = driver.connect(database.getConnectionUri(), Options.NONE)) {
conn.setSchema(database.getSchemaName());
try (Statement stmt = conn.createStatement()) {
@@ -208,7 +214,7 @@ void testRelationalConnectionSetLogOnThenOff() throws Exception {
@Test
void testRelationalConnectionSetLogIsOverriddenByQueryOption() throws Exception {
- final var driver = (RelationalDriver) DriverManager.getDriver(database.getConnectionUri().toString());
+ final var driver = relationalExtension.getDriver();
try (RelationalConnection conn = driver.connect(database.getConnectionUri(), Options.NONE)) {
conn.setSchema(database.getSchemaName());
conn.setOption(Options.Name.LOG_QUERY, false);
@@ -226,7 +232,7 @@ void testRelationalConnectionSetLogIsOverriddenByQueryOption() throws Exception
@Test
void testRelationalConnectionSetLogIsOverriddenByExecuteContinuationQueryOption() throws Exception {
insertRows();
- final var driver = (RelationalDriver) DriverManager.getDriver(database.getConnectionUri().toString());
+ final var driver = relationalExtension.getDriver();
try (RelationalConnection conn = driver.connect(database.getConnectionUri(), Options.NONE)) {
Continuation continuation;
conn.setSchema(database.getSchemaName());
@@ -254,7 +260,7 @@ void testRelationalConnectionSetLogIsOverriddenByExecuteContinuationQueryOption(
@ValueSource(booleans = {true, false})
void testRelationalConnectionSetLogWithExecuteContinuation(boolean setLogging) throws Exception {
insertRows();
- final var driver = (RelationalDriver) DriverManager.getDriver(database.getConnectionUri().toString());
+ final var driver = relationalExtension.getDriver();
try (RelationalConnection conn = driver.connect(database.getConnectionUri(), Options.NONE)) {
Continuation continuation;
conn.setSchema(database.getSchemaName());
@@ -298,7 +304,7 @@ void testLogQueryBecauseQueryIsSlow() throws Exception {
resultSet.next();
}
Assertions.assertThat(logAppender.getLogEvents()).isEmpty();
- final var driver = (RelationalDriver) DriverManager.getDriver(database.getConnectionUri().toString());
+ final var driver = relationalExtension.getDriver();
try (RelationalConnection conn = driver.connect(database.getConnectionUri(), Options.builder().withOption(Options.Name.LOG_SLOW_QUERY_THRESHOLD_MICROS, 1L).build())) {
conn.setSchema(database.getSchemaName());
try (PreparedStatement ps = conn.prepareStatement("SELECT NAME FROM RESTAURANT")) {
@@ -316,7 +322,7 @@ void testLogQueryBecauseQueryIsSlowRemovesLiterals() throws Exception {
resultSet.next();
}
Assertions.assertThat(logAppender.getLogEvents()).isEmpty();
- final var driver = (RelationalDriver) DriverManager.getDriver(database.getConnectionUri().toString());
+ final var driver = relationalExtension.getDriver();
try (RelationalConnection conn = driver.connect(database.getConnectionUri(), Options.builder().withOption(Options.Name.LOG_SLOW_QUERY_THRESHOLD_MICROS, 1L).build())) {
conn.setSchema(database.getSchemaName());
try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM RESTAURANT WHERE \"NAME\" = 'restaurant 1'")) {
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/QueryPropertiesTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/QueryPropertiesTest.java
index 8e6d94fe6b..e95ffcb7b8 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/QueryPropertiesTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/QueryPropertiesTest.java
@@ -31,7 +31,6 @@
import com.apple.foundationdb.relational.api.KeySet;
import com.apple.foundationdb.relational.api.Options;
import com.apple.foundationdb.relational.api.RelationalConnection;
-import com.apple.foundationdb.relational.api.RelationalDriver;
import com.apple.foundationdb.relational.api.RelationalResultSet;
import com.apple.foundationdb.relational.api.RelationalStatement;
import com.apple.foundationdb.relational.api.exceptions.RelationalException;
@@ -45,7 +44,6 @@
import org.junit.jupiter.api.extension.RegisterExtension;
import javax.annotation.Nonnull;
-import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
@@ -57,7 +55,7 @@ public class QueryPropertiesTest {
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(QueryPropertiesTest.class, TestSchemas.restaurant());
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, QueryPropertiesTest.class, TestSchemas.restaurant());
@Test
void verifyExecuteAndScanPropertiesGivenQueryProperties() throws SQLException {
@@ -93,7 +91,7 @@ void scanWithLimit() throws RelationalException, SQLException {
}
List testScan(Options options, long firstRestNo) throws RelationalException, SQLException {
- final var driver = (RelationalDriver) DriverManager.getDriver(database.getConnectionUri().toString());
+ final var driver = relationalExtension.getDriver();
try (RelationalConnection conn = driver.connect(database.getConnectionUri(), options)) {
conn.setSchema("TEST_SCHEMA");
try (RelationalStatement s = conn.createStatement()) {
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RecordTypeKeyTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RecordTypeKeyTest.java
index 21cd11b2cb..4eca57ee5d 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RecordTypeKeyTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RecordTypeKeyTest.java
@@ -43,7 +43,7 @@ public class RecordTypeKeyTest {
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension,
RecordTypeKeyTest.class,
"""
CREATE TABLE restaurant_review (reviewer bigint, rating bigint, SINGLE ROW ONLY)
@@ -54,7 +54,7 @@ CREATE TABLE restaurant_tag (tag string, weight bigint, PRIMARY KEY(tag))
@RegisterExtension
@Order(2)
- public final RelationalConnectionRule connection = new RelationalConnectionRule(database::getConnectionUri)
+ public final RelationalConnectionRule connection = new RelationalConnectionRule(relationalExtension, database::getConnectionUri)
.withOptions(Options.NONE)
.withSchema("TEST_SCHEMA");
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RecordTypeTableSerDeTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RecordTypeTableSerDeTest.java
index 6236e5c082..52e4a321db 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RecordTypeTableSerDeTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RecordTypeTableSerDeTest.java
@@ -45,14 +45,14 @@ class RecordTypeTableSerDeTest {
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule db = new SimpleDatabaseRule(RecordTypeTableSerDeTest.class,
+ public final SimpleDatabaseRule db = new SimpleDatabaseRule(relationalExtension, RecordTypeTableSerDeTest.class,
"""
CREATE TABLE t1(a bigint, b string, c bytes, d boolean, e integer, f float, g double, h uuid, PRIMARY KEY(a))
""");
@RegisterExtension
@Order(2)
- public final RelationalConnectionRule connection = new RelationalConnectionRule(db::getConnectionUri)
+ public final RelationalConnectionRule connection = new RelationalConnectionRule(relationalExtension, db::getConnectionUri)
.withOptions(Options.NONE)
.withSchema(db.getSchemaName());
diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RelationalArrayTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RelationalArrayTest.java
index 7b7d08030c..3be3c3fdad 100644
--- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RelationalArrayTest.java
+++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RelationalArrayTest.java
@@ -22,7 +22,6 @@
import com.apple.foundationdb.relational.api.EmbeddedRelationalArray;
import com.apple.foundationdb.relational.api.EmbeddedRelationalStruct;
-import com.apple.foundationdb.relational.api.RelationalPreparedStatement;
import com.apple.foundationdb.relational.api.RelationalStruct;
import com.apple.foundationdb.relational.api.exceptions.ErrorCode;
import com.apple.foundationdb.relational.api.metadata.DataType;
@@ -39,7 +38,6 @@
import org.junit.jupiter.params.provider.MethodSource;
import javax.annotation.Nonnull;
-import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
@@ -47,6 +45,7 @@
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;
+import java.net.URI;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -82,10 +81,10 @@ CREATE TABLE B(pk integer, uuid_null uuid array, primary key(pk))
@RegisterExtension
@Order(1)
- public final SimpleDatabaseRule database = new SimpleDatabaseRule(RelationalArrayTest.class, SCHEMA_TEMPLATE);
+ public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, RelationalArrayTest.class, SCHEMA_TEMPLATE);
public void insertQuery(@Nonnull String q) throws SQLException {
- try (final var conn = DriverManager.getConnection(database.getConnectionUri().toString())) {
+ try (final var conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString()))) {
conn.setSchema(database.getSchemaName());
conn.setAutoCommit(true);
try (final var s = conn.createStatement()) {
@@ -158,10 +157,10 @@ void testInsertArraysViaQueryPreparedStatement() throws SQLException {
"?string_not_null, ?bytes_null, ?bytes_not_null, ?struct_null, ?struct_not_null, ?uuid_null, " +
"?uuid_not_null)";
- try (final var conn = DriverManager.getConnection(database.getConnectionUri().toString())) {
+ try (final var conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString()))) {
conn.setSchema(database.getSchemaName());
conn.setAutoCommit(true);
- try (final var ps = ((RelationalPreparedStatement) conn.prepareStatement(statement))) {
+ try (final var ps = conn.prepareStatement(statement)) {
ps.setInt("pk", 1);
ps.setArray("boolean_null", EmbeddedRelationalArray.newBuilder().addAll(true, false).build());
ps.setArray("boolean_not_null", EmbeddedRelationalArray.newBuilder().addAll(true, false).build());
@@ -185,10 +184,10 @@ void testInsertArraysViaQueryPreparedStatement() throws SQLException {
}
}
- try (final var conn = DriverManager.getConnection(database.getConnectionUri().toString())) {
+ try (final var conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString()))) {
conn.setSchema(database.getSchemaName());
conn.setAutoCommit(true);
- try (final var ps = ((RelationalPreparedStatement) conn.prepareStatement(statement))) {
+ try (final var ps = conn.prepareStatement(statement)) {
ps.setInt("pk", 2);
ps.setNull("boolean_null", Types.ARRAY);
ps.setArray("boolean_not_null", EmbeddedRelationalArray.newBuilder().addAll(true, false).build());
@@ -212,10 +211,10 @@ void testInsertArraysViaQueryPreparedStatement() throws SQLException {
}
}
- try (final var conn = DriverManager.getConnection(database.getConnectionUri().toString())) {
+ try (final var conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString()))) {
conn.setSchema(database.getSchemaName());
conn.setAutoCommit(true);
- try (final var ps = ((RelationalPreparedStatement) conn.prepareStatement(statement))) {
+ try (final var ps = conn.prepareStatement(statement)) {
ps.setInt("pk", 2);
ps.setArray("boolean_null", EmbeddedRelationalArray.newBuilder().addAll(true, false).build());
ps.setNull("boolean_not_null", Types.ARRAY);
@@ -253,10 +252,10 @@ void testInsertEmptyArrayViaQueryPreparedStatement() throws SQLException {
DataType.StructType.Field.from("B", DataType.StringType.nullable(), 2)),
false)).build();
- try (final var conn = DriverManager.getConnection(database.getConnectionUri().toString())) {
+ try (final var conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString()))) {
conn.setSchema(database.getSchemaName());
conn.setAutoCommit(true);
- try (final var ps = ((RelationalPreparedStatement) conn.prepareStatement(statement))) {
+ try (final var ps = conn.prepareStatement(statement)) {
ps.setInt("pk", 1);
ps.setArray("boolean_not_null", EmbeddedRelationalArray.newBuilder(DataType.BooleanType.notNullable()).build());
ps.setArray("integer_not_null", EmbeddedRelationalArray.newBuilder(DataType.IntegerType.notNullable()).build());
@@ -270,7 +269,7 @@ void testInsertEmptyArrayViaQueryPreparedStatement() throws SQLException {
Assertions.assertEquals(1, ps.executeUpdate());
}
- try (final var ps = (RelationalPreparedStatement) conn.prepareStatement("SELECT * from T where pk = 1")) {
+ try (final var ps = conn.prepareStatement("SELECT * from T where pk = 1")) {
ResultSetAssert.assertThat(ps.executeQuery())
.hasNextRow()
.hasColumn("boolean_not_null", List.of())
@@ -288,10 +287,10 @@ void testInsertEmptyArrayViaQueryPreparedStatement() throws SQLException {
@Test
void testMoreParametersThanColumns() throws SQLException {
- try (final var conn = DriverManager.getConnection(database.getConnectionUri().toString())) {
+ try (final var conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString()))) {
conn.setSchema(database.getSchemaName());
conn.setAutoCommit(true);
- try (final var ps = ((RelationalPreparedStatement) conn.prepareStatement("INSERT INTO T (pk) VALUES (?pk, ?boolean_null, ?boolean_not_null)"))) {
+ try (final var ps = conn.prepareStatement("INSERT INTO T (pk) VALUES (?pk, ?boolean_null, ?boolean_not_null)")) {
ps.setInt("pk", 1);
ps.setArray("boolean_null", EmbeddedRelationalArray.newBuilder().addAll(true, false).build());
ps.setArray("boolean_not_null", EmbeddedRelationalArray.newBuilder().addAll(true, false).build());
@@ -303,10 +302,10 @@ void testMoreParametersThanColumns() throws SQLException {
@Test
void testNonNullViolation() throws SQLException {
- try (final var conn = DriverManager.getConnection(database.getConnectionUri().toString())) {
+ try (final var conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString()))) {
conn.setSchema(database.getSchemaName());
conn.setAutoCommit(true);
- try (final var ps = ((RelationalPreparedStatement) conn.prepareStatement("INSERT INTO T (pk) VALUES (?pk)"))) {
+ try (final var ps = conn.prepareStatement("INSERT INTO T (pk) VALUES (?pk)")) {
ps.setInt("pk", 1);
RelationalAssertions.assertThrowsSqlException(ps::executeUpdate).containsInMessage("violates not-null constraint")
.hasErrorCode(ErrorCode.NOT_NULL_VIOLATION);
@@ -316,13 +315,13 @@ void testNonNullViolation() throws SQLException {
@Test
void testNullFieldsGetAutomaticallyFilled() throws SQLException {
- try (final var conn = DriverManager.getConnection(database.getConnectionUri().toString())) {
+ try (final var conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString()))) {
conn.setSchema(database.getSchemaName());
conn.setAutoCommit(true);
- try (final var ps = ((RelationalPreparedStatement) conn.prepareStatement("INSERT INTO T (pk, boolean_not_null, " +
+ try (final var ps = conn.prepareStatement("INSERT INTO T (pk, boolean_not_null, " +
"integer_not_null, bigint_not_null, float_not_null, double_not_null, string_not_null, bytes_not_null, " +
"struct_not_null, uuid_not_null) VALUES (?pk, ?boolean_not_null, ?integer_not_null, ?bigint_not_null, " +
- "?float_not_null, ?double_not_null, ?string_not_null, ?bytes_not_null, ?struct_not_null, ?uuid_not_null)"))) {
+ "?float_not_null, ?double_not_null, ?string_not_null, ?bytes_not_null, ?struct_not_null, ?uuid_not_null)")) {
ps.setInt("pk", 2);
ps.setArray("boolean_not_null", EmbeddedRelationalArray.newBuilder().addAll(true, false).build());
ps.setArray("integer_not_null", EmbeddedRelationalArray.newBuilder().addAll(11, 22).build());
@@ -336,7 +335,7 @@ void testNullFieldsGetAutomaticallyFilled() throws SQLException {
assertEquals(1, ps.executeUpdate());
}
- try (final var ps = (RelationalPreparedStatement) conn.prepareStatement("SELECT * from T where pk = 2")) {
+ try (final var ps = conn.prepareStatement("SELECT * from T where pk = 2")) {
ResultSetAssert.assertThat(ps.executeQuery())
.hasNextRow()
.hasColumn("boolean_null", null)
@@ -354,13 +353,13 @@ void testNullFieldsGetAutomaticallyFilled() throws SQLException {
@Test
void testColumnRequiresValue() throws SQLException {
- try (final var conn = DriverManager.getConnection(database.getConnectionUri().toString())) {
+ try (final var conn = relationalExtension.getDriver().connect(URI.create(database.getConnectionUri().toString()))) {
conn.setSchema(database.getSchemaName());
conn.setAutoCommit(true);
- try (final var ps = ((RelationalPreparedStatement) conn.prepareStatement("INSERT INTO T (pk, boolean_not_null, " +
+ try (final var ps = conn.prepareStatement("INSERT INTO T (pk, boolean_not_null, " +
"integer_not_null, bigint_not_null, float_not_null, double_not_null, string_not_null, bytes_not_null, " +
"struct_not_null) VALUES (?pk, ?boolean_not_null, ?integer_not_null, ?bigint_not_null, ?float_not_null, " +
- "?double_not_null, ?string_not_null)"))) {
+ "?double_not_null, ?string_not_null)")) {
ps.setInt("pk", 2);
ps.setArray("boolean_not_null", EmbeddedRelationalArray.newBuilder().addAll(true, false).build());
ps.setArray("integer_not_null", EmbeddedRelationalArray.newBuilder().addAll(11, 22).build());
@@ -405,7 +404,7 @@ void testArrays(int nullArrayIdx, int nonNullArrayIdx, List