diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 5a6e05d370..0d8ae71715 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -27,7 +27,7 @@ The Guava dependency version has been updated to 31.1. Projects may need to chec * **Performance** Improvement 5 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN) * **Feature** Feature 1 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN) * **Feature** Feature 2 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN) -* **Feature** Feature 3 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN) +* **Feature** Non-idempotent target indexes can now be built from an existing index [(Issue #1430)](https://github.com/FoundationDB/fdb-record-layer/issues/1430) * **Feature** Feature 4 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN) * **Feature** Feature 5 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN) * **Feature** Support planning aggregate indexes in Cascades. [(Issue #1885)](https://github.com/FoundationDB/fdb-record-layer/issues/1885) 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 a9e2efa0f9..d9714422a4 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 @@ -42,6 +42,7 @@ import com.apple.foundationdb.record.ExecuteProperties; import com.apple.foundationdb.record.ExecuteState; import com.apple.foundationdb.record.FunctionNames; +import com.apple.foundationdb.record.IndexBuildProto; import com.apple.foundationdb.record.IndexEntry; import com.apple.foundationdb.record.IndexScanType; import com.apple.foundationdb.record.IndexState; @@ -605,22 +606,12 @@ private void updateSecondaryIndexes(@Nullable final FDBIndex for (Index index : indexes) { final IndexMaintainer maintainer = getIndexMaintainer(index); final CompletableFuture future; - if (!maintainer.isIdempotent() && isIndexWriteOnly(index)) { - // In this case, the index is still being built, so we are not - // going to update the record unless the rebuild job has already - // gotten to this range. - final Tuple primaryKey = newRecord == null ? oldRecord.getPrimaryKey() : newRecord.getPrimaryKey(); - future = maintainer.addedRangeWithKey(primaryKey) - .thenCompose(present -> { - if (present) { - return maintainer.update(oldRecord, newRecord); - } else { - return AsyncUtil.DONE; - } - }); - if (!MoreAsyncUtil.isCompletedNormally(future)) { - futures.add(future); - } + if (isIndexWriteOnly(index)) { + // In this case, the index is still being built. For some index + // types, the index update needs to check whether indexing + // process has already built the relevant ranges, and it + // may adjust the way the index is built in response. + future = maintainer.updateWhileWriteOnly(oldRecord, newRecord); } else { future = maintainer.update(oldRecord, newRecord); } @@ -3499,6 +3490,32 @@ public CompletableFuture getIndexBuildStateAsync(Index index) { return IndexBuildState.loadIndexBuildStateAsync(this, index); } + @API(API.Status.INTERNAL) + @Nonnull + public CompletableFuture loadIndexBuildStampAsync(Index index) { + byte[] stampKey = IndexingBase.indexBuildTypeSubspace(this, index).pack(); + return ensureContextActive().get(stampKey).thenApply(serializedStamp -> { + if (serializedStamp == null) { + return null; + } + try { + return IndexBuildProto.IndexBuildIndexingStamp.parseFrom(serializedStamp); + } catch (InvalidProtocolBufferException ex) { + RecordCoreException protoEx = new RecordCoreException("invalid indexing type stamp", + LogMessageKeys.INDEX_NAME, index.getName(), + LogMessageKeys.ACTUAL, ByteArrayUtil2.loggable(serializedStamp)); + protoEx.initCause(ex); + throw protoEx; + } + }); + } + + @API(API.Status.INTERNAL) + public void saveIndexBuildStamp(Index index, IndexBuildProto.IndexBuildIndexingStamp stamp) { + byte[] stampKey = IndexingBase.indexBuildTypeSubspace(this, index).pack(); + ensureContextActive().set(stampKey, stamp.toByteArray()); + } + // Remove any indexes that do not match the filter. // NOTE: This assumes that the filter will not filter out any indexes if all indexes are readable. private List sanitizeIndexes(@Nonnull List indexes, @Nonnull Predicate filter) { diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/IndexMaintainer.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/IndexMaintainer.java index 9b6924e5d5..cd5e1139be 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/IndexMaintainer.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/IndexMaintainer.java @@ -121,6 +121,10 @@ public RecordCursor scan(@Nonnull IndexScanBounds scanBounds, public abstract CompletableFuture update(@Nullable FDBIndexableRecord oldRecord, @Nullable FDBIndexableRecord newRecord); + @Nonnull + public abstract CompletableFuture updateWhileWriteOnly(@Nullable FDBIndexableRecord oldRecord, + @Nullable FDBIndexableRecord newRecord); + /** * Scans through the list of uniqueness violations within the database. diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/IndexingBase.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/IndexingBase.java index a878c56d79..8ecd9ae49e 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/IndexingBase.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/IndexingBase.java @@ -47,7 +47,6 @@ import com.apple.foundationdb.subspace.Subspace; import com.apple.foundationdb.tuple.ByteArrayUtil2; import com.apple.foundationdb.tuple.Tuple; -import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; @@ -401,35 +400,32 @@ public void enforceStampOverwrite() { @SuppressWarnings("PMD.CloseResource") private CompletableFuture setIndexingTypeOrThrow(FDBRecordStore store, boolean continuedBuild) { // continuedBuild is set if this session isn't a continuation of a previous indexing - Transaction transaction = store.getContext().ensureActive(); IndexBuildProto.IndexBuildIndexingStamp indexingTypeStamp = getIndexingTypeStamp(store); - return forEachTargetIndex(index -> setIndexingTypeOrThrow(store, continuedBuild, transaction, index, indexingTypeStamp)); + return forEachTargetIndex(index -> setIndexingTypeOrThrow(store, continuedBuild, index, indexingTypeStamp)); } @Nonnull - private CompletableFuture setIndexingTypeOrThrow(FDBRecordStore store, boolean continuedBuild, Transaction transaction, Index index, IndexBuildProto.IndexBuildIndexingStamp indexingTypeStamp) { - byte[] stampKey = indexBuildTypeSubspace(store, index).getKey(); + private CompletableFuture setIndexingTypeOrThrow(FDBRecordStore store, boolean continuedBuild, Index index, IndexBuildProto.IndexBuildIndexingStamp indexingTypeStamp) { if (forceStampOverwrite && !continuedBuild) { // Fresh session + overwrite = no questions asked - transaction.set(stampKey, indexingTypeStamp.toByteArray()); + store.saveIndexBuildStamp(index, indexingTypeStamp); return AsyncUtil.DONE; } - return transaction.get(stampKey) - .thenCompose(bytes -> { - if (bytes == null) { + return store.loadIndexBuildStampAsync(index) + .thenCompose(savedStamp -> { + if (savedStamp == null) { if (continuedBuild && indexingTypeStamp.getMethod() != IndexBuildProto.IndexBuildIndexingStamp.Method.BY_RECORDS) { // backward compatibility - maybe continuing an old BY_RECORD session return isWriteOnlyButNoRecordScanned(store, index) - .thenCompose(noRecordScanned -> throwAsByRecordsUnlessNoRecordWasScanned(noRecordScanned, transaction, index, stampKey, indexingTypeStamp)); + .thenCompose(noRecordScanned -> throwAsByRecordsUnlessNoRecordWasScanned(noRecordScanned, store, index, indexingTypeStamp)); } // Here: either not a continuedBuild (new session), or a BY_RECORD session (allowed to overwrite the null stamp) - transaction.set(stampKey, indexingTypeStamp.toByteArray()); + store.saveIndexBuildStamp(index, indexingTypeStamp); return AsyncUtil.DONE; } // Here: has non-null type stamp - IndexBuildProto.IndexBuildIndexingStamp savedStamp = parseTypeStampOrThrow(bytes); if (indexingTypeStamp.equals(savedStamp)) { // A matching stamp is already there - One less thing to worry about return AsyncUtil.DONE; @@ -438,14 +434,14 @@ private CompletableFuture setIndexingTypeOrThrow(FDBRecordStore store, boo indexingTypeStamp.getMethod() == IndexBuildProto.IndexBuildIndexingStamp.Method.BY_RECORDS && savedStamp.getMethod() == IndexBuildProto.IndexBuildIndexingStamp.Method.MULTI_TARGET_BY_RECORDS) { // Special case: partly built with multi target, but may be continued indexing on its own - transaction.set(stampKey, indexingTypeStamp.toByteArray()); + store.saveIndexBuildStamp(index, indexingTypeStamp); return AsyncUtil.DONE; } if (forceStampOverwrite) { // and a continued Build // check if partly built return isWriteOnlyButNoRecordScanned(store, index) .thenCompose(noRecordScanned -> - throwUnlessNoRecordWasScanned(noRecordScanned, transaction, index, stampKey, indexingTypeStamp, + throwUnlessNoRecordWasScanned(noRecordScanned, store, index, indexingTypeStamp, savedStamp, continuedBuild)); } // fall down to exception @@ -454,8 +450,9 @@ private CompletableFuture setIndexingTypeOrThrow(FDBRecordStore store, boo } @Nonnull - private CompletableFuture throwAsByRecordsUnlessNoRecordWasScanned(boolean noRecordScanned, Transaction transaction, - Index index, byte[] stampKey, + private CompletableFuture throwAsByRecordsUnlessNoRecordWasScanned(boolean noRecordScanned, + FDBRecordStore store, + Index index, IndexBuildProto.IndexBuildIndexingStamp indexingTypeStamp) { // A complicated way to reduce complexity. if (noRecordScanned) { @@ -465,7 +462,7 @@ private CompletableFuture throwAsByRecordsUnlessNoRecordWasScanned(boolean .addKeysAndValues(common.indexLogMessageKeyValues()) .toString()); } - transaction.set(stampKey, indexingTypeStamp.toByteArray()); + store.saveIndexBuildStamp(index, indexingTypeStamp); return AsyncUtil.DONE; } // Here: there is no type stamp, but indexing is ongoing. For backward compatibility reasons, we'll consider it a BY_RECORDS stamp @@ -479,15 +476,16 @@ private CompletableFuture throwAsByRecordsUnlessNoRecordWasScanned(boolean } @Nonnull - private CompletableFuture throwUnlessNoRecordWasScanned(boolean noRecordScanned, Transaction transaction, - Index index, byte[] stampKey, + private CompletableFuture throwUnlessNoRecordWasScanned(boolean noRecordScanned, + FDBRecordStore store, + Index index, IndexBuildProto.IndexBuildIndexingStamp indexingTypeStamp, IndexBuildProto.IndexBuildIndexingStamp savedStamp, boolean continuedBuild) { // Ditto (a complicated way to reduce complexity) if (noRecordScanned) { // we can safely overwrite the previous type stamp - transaction.set(stampKey, indexingTypeStamp.toByteArray()); + store.saveIndexBuildStamp(index, indexingTypeStamp); return AsyncUtil.DONE; } // A force overwrite cannot be allowed when partly built @@ -543,19 +541,6 @@ private CompletableFuture setScrubberTypeOrThrow(FDBRecordStore store) { abstract CompletableFuture buildIndexInternalAsync(); - private IndexBuildProto.IndexBuildIndexingStamp parseTypeStampOrThrow(byte[] bytes) { - try { - return IndexBuildProto.IndexBuildIndexingStamp.parseFrom(bytes); - } catch (InvalidProtocolBufferException ex) { - RecordCoreException protoEx = new RecordCoreException("invalid indexing type stamp", - LogMessageKeys.INDEX_NAME, common.getTargetIndexesNames(), - LogMessageKeys.INDEXER_ID, common.getUuid(), - LogMessageKeys.ACTUAL, bytes); - protoEx.initCause(ex); - throw protoEx; - } - } - private CompletableFuture isWriteOnlyButNoRecordScanned(FDBRecordStore store, Index index) { RangeSet rangeSet = new RangeSet(store.indexRangeSubspace(index)); AsyncIterator ranges = rangeSet.missingRanges(store.ensureContextActive()).iterator(); diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/IndexingByIndex.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/IndexingByIndex.java index c038e03df0..1fda3a3072 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/IndexingByIndex.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/IndexingByIndex.java @@ -151,7 +151,7 @@ private CompletableFuture buildRangeOnly(@Nonnull FDBRecordStore store, final IndexMaintainer maintainer = store.getIndexMaintainer(index); // idempotence - We could have verified it at the first iteration only, but the repeating checks seem harmless - validateOrThrowEx(maintainer.isIdempotent(), "target index is not idempotent"); + // validateOrThrowEx(maintainer.isIdempotent(), "target index is not idempotent"); // readability - This method shouldn't block if one has already opened the record store (as we did) Index srcIndex = getSourceIndex(store.getRecordMetaData()); validateOrThrowEx(store.isIndexScannable(srcIndex), "source index is not scannable"); @@ -160,7 +160,7 @@ private CompletableFuture buildRangeOnly(@Nonnull FDBRecordStore store, AsyncIterator ranges = rangeSet.missingRanges(store.ensureContextActive()).iterator(); final ExecuteProperties.Builder executeProperties = ExecuteProperties.newBuilder() - .setIsolationLevel(IsolationLevel.SNAPSHOT) + .setIsolationLevel(maintainer.isIdempotent() ? IsolationLevel.SNAPSHOT : IsolationLevel.SERIALIZABLE) .setReturnedRowLimit(getLimit() + 1); // respect limit in this path; +1 allows a continuation item final ScanProperties scanProperties = new ScanProperties(executeProperties.build()); @@ -179,10 +179,9 @@ private CompletableFuture buildRangeOnly(@Nonnull FDBRecordStore store, final AtomicReference>> lastResult = new AtomicReference<>(RecordCursorResult.exhausted()); final AtomicBoolean hasMore = new AtomicBoolean(true); - final boolean isIdempotent = true ; // Note that currently indexing by index is online implemented for idempotent indexes return iterateRangeOnly(store, cursor, this::getRecordIfTypeMatch, - lastResult, hasMore, recordsScanned, isIdempotent) + lastResult, hasMore, recordsScanned, maintainer.isIdempotent()) .thenApply(vignore -> hasMore.get() ? lastResult.get().get().getIndexEntry().getKey() : rangeEnd) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/indexes/NoOpIndexMaintainer.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/indexes/NoOpIndexMaintainer.java index 9b24b5ac19..62b6fda9b7 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/indexes/NoOpIndexMaintainer.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/indexes/NoOpIndexMaintainer.java @@ -73,6 +73,12 @@ public CompletableFuture update(@Nullable FDBIndexable return AsyncUtil.DONE; } + @Nonnull + @Override + public CompletableFuture updateWhileWriteOnly(@Nullable final FDBIndexableRecord oldRecord, @Nullable final FDBIndexableRecord newRecord) { + return AsyncUtil.DONE; + } + @Nonnull @Override public RecordCursor scanUniquenessViolations(@Nonnull TupleRange range, @Nullable byte[] continuation, @Nonnull ScanProperties scanProperties) { diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/indexes/StandardIndexMaintainer.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/indexes/StandardIndexMaintainer.java index 94d04bd819..5ca3b5c955 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/indexes/StandardIndexMaintainer.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/indexes/StandardIndexMaintainer.java @@ -72,6 +72,7 @@ import com.apple.foundationdb.tuple.ByteArrayUtil; import com.apple.foundationdb.tuple.Tuple; import com.apple.foundationdb.tuple.TupleHelpers; +import com.google.common.base.Verify; import com.google.protobuf.Message; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; @@ -251,6 +252,77 @@ public CompletableFuture update(@Nullable final FDBInd return future; } + @Override + public CompletableFuture updateWhileWriteOnly(@Nullable final FDBIndexableRecord oldRecord, @Nullable final FDBIndexableRecord newRecord) { + if (isIdempotent()) { + return update(oldRecord, newRecord); + } + return state.store.loadIndexBuildStampAsync(state.index).thenCompose(stamp -> { + if (stamp == null) { + // The index build has not begun, so the entire range is unbuilt, and thus + // the index update should be skipped + return updateWriteOnlyByRecords(oldRecord, newRecord); + } + switch (stamp.getMethod()) { + case BY_RECORDS: + return updateWriteOnlyByRecords(oldRecord, newRecord); + case BY_INDEX: + Object sourceIndexKey = Tuple.fromBytes(stamp.getSourceIndexSubspaceKey().toByteArray()).get(0); + Index sourceIndex = state.store.getRecordMetaData().getIndexFromSubspaceKey(sourceIndexKey); + return updateWriteOnlyByIndex(sourceIndex, oldRecord, newRecord); + default: + throw new RecordCoreException("unable to update write-only index with current type stamp") + .addLogInfo("stamp", stamp); + } + }); + } + + private CompletableFuture updateWriteOnlyByRecords(@Nullable final FDBIndexableRecord oldRecord, @Nullable final FDBIndexableRecord newRecord) { + Tuple primaryKey = oldRecord == null ? Verify.verifyNotNull(newRecord).getPrimaryKey() : oldRecord.getPrimaryKey(); + return addedRangeWithKey(primaryKey).thenCompose(inRange -> + inRange ? update(oldRecord, newRecord) : AsyncUtil.DONE); + } + + private CompletableFuture updateWriteOnlyByIndex(@Nonnull Index sourceIndex, @Nullable final FDBIndexableRecord oldRecord, @Nullable final FDBIndexableRecord newRecord) { + IndexMaintainer sourceIndexMaintainer = state.store.getIndexMaintainer(sourceIndex); + Tuple oldEntryKey = evaluateSingletonIndexKey(sourceIndex, sourceIndexMaintainer, oldRecord); + Tuple newEntryKey = evaluateSingletonIndexKey(sourceIndex, sourceIndexMaintainer, newRecord); + if (oldEntryKey != null && newEntryKey != null) { + if (oldEntryKey.equals(newEntryKey)) { + return addedRangeWithKey(oldEntryKey).thenCompose(inRange -> + inRange ? update(oldRecord, newRecord) : AsyncUtil.DONE); + } else { + return addedRangeWithKey(oldEntryKey) + .thenCompose(oldInRange -> oldInRange ? update(oldRecord, null) : AsyncUtil.DONE) + .thenCompose(ignore -> addedRangeWithKey(newEntryKey)) + .thenCompose(newInRange -> newInRange ? update(null, newRecord) : AsyncUtil.DONE); + } + } else { + Tuple entryKey = oldEntryKey == null ? newEntryKey : oldEntryKey; + if (entryKey == null) { + return AsyncUtil.DONE; + } else { + return addedRangeWithKey(entryKey).thenCompose(inRange -> + inRange ? update(oldRecord, newRecord) : AsyncUtil.DONE); + } + } + } + + @Nullable + private static Tuple evaluateSingletonIndexKey(Index index, IndexMaintainer maintainer, @Nullable FDBIndexableRecord record) { + if (record == null) { + return null; + } + List entries = maintainer.filteredIndexEntries(record); + if (entries == null || entries.isEmpty()) { + return null; + } else if (entries.size() != 1) { + throw new RecordCoreException("index produced incorrect number of entries for use as source index"); + } + IndexEntry entry = entries.get(0); + return FDBRecordStoreBase.indexEntryKey(index, entry.getKey(), record.getPrimaryKey()); + } + /** * Filter out index keys according to {@link IndexMaintenanceFilter}. * Keys that do not pass the filter will not be stored / removed from the index. diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerIndexFromIndexTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerIndexFromIndexTest.java index a72eec0603..445462a26c 100644 --- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerIndexFromIndexTest.java +++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/OnlineIndexerIndexFromIndexTest.java @@ -224,10 +224,10 @@ public void testIndexFromIndexNoFallback() { .setTimer(timer) .build()) { - assertThrows(IndexingByIndex.ValidationException.class, indexBuilder::buildIndex); + indexBuilder.buildIndex(true); } - assertEquals(0, timer.getCount(FDBStoreTimer.Counts.ONLINE_INDEX_BUILDER_RECORDS_SCANNED)); - assertEquals(0, timer.getCount(FDBStoreTimer.Counts.ONLINE_INDEX_BUILDER_RECORDS_INDEXED)); + assertEquals(numRecords, timer.getCount(FDBStoreTimer.Counts.ONLINE_INDEX_BUILDER_RECORDS_SCANNED)); + assertEquals(numRecords, timer.getCount(FDBStoreTimer.Counts.ONLINE_INDEX_BUILDER_RECORDS_INDEXED)); } @Test diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/TerribleIndexMaintainer.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/TerribleIndexMaintainer.java index 65181a8ea8..130e84ebcc 100644 --- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/TerribleIndexMaintainer.java +++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/TerribleIndexMaintainer.java @@ -94,6 +94,12 @@ public CompletableFuture update(@Nullable FDBIndexable } } + @Nonnull + @Override + public CompletableFuture updateWhileWriteOnly(@Nullable final FDBIndexableRecord oldRecord, @Nullable final FDBIndexableRecord newRecord) { + return AsyncUtil.DONE; + } + @Nonnull @Override public RecordCursor scanUniquenessViolations(@Nonnull TupleRange range, @Nullable byte[] continuation, @Nonnull ScanProperties scanProperties) {