Skip to content

Commit 2c7b185

Browse files
authored
Synthetic record orphan policy (#3271)
Add support for `orphanPolicy` when loading synthetic records. This is necessary for the ability to scrub indexes with synthetic records as dangling index entries, by definition, have no records to back them up. As part of this PR we've also disabled the repair of synthetic records as part of the scrubbing until we test this feature more thoroughly. Also of note: Implementors of the `EXPERIMENTAL` `SyntheticRecordType` interface will see a change that requires the implementation of another method (or add the new parameter to the existing one). As far as the "breaking change" label: The scrubbing for dangling index entries now uses a `RETURN` policy (default was `ERROR`) which means that previous requests that used to fail now pass (if the record was missing). Also, any request to scrub synthetic record with repair `on` will now fail.
1 parent cf66096 commit 2c7b185

File tree

11 files changed

+491
-99
lines changed

11 files changed

+491
-99
lines changed

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/JoinedRecordType.java

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStore;
3030
import com.apple.foundationdb.record.provider.foundationdb.FDBStoredRecord;
3131
import com.apple.foundationdb.record.provider.foundationdb.FDBSyntheticRecord;
32+
import com.apple.foundationdb.record.provider.foundationdb.IndexOrphanBehavior;
3233
import com.apple.foundationdb.record.provider.foundationdb.RecordDoesNotExistException;
3334
import com.apple.foundationdb.tuple.Tuple;
3435
import com.google.protobuf.Descriptors;
@@ -39,9 +40,33 @@
3940
import java.util.Map;
4041
import java.util.concurrent.CompletableFuture;
4142
import java.util.concurrent.ConcurrentHashMap;
43+
import java.util.concurrent.atomic.AtomicBoolean;
4244

4345
/**
4446
* A <i>synthetic</i> record type representing the indexable result of <em>joining</em> stored records.
47+
* <p>
48+
* Joined record represents a collection of <i>joined constituents</i>, each represented by another record in the database.
49+
* The constituents are joined together by a <i>join condition</i>: in this case, a set of fields that are tested for
50+
* equality across all constituents.
51+
* The joined record primary key is represented by a Tuple as follows:
52+
* <pre>
53+
* [{record type}, {elements of constituent 1 PK}, ... {elements of constituent n PK}]
54+
* </pre>
55+
* When loading a joined record, the joined record primary key is used to iterate and load each constituent, eventually
56+
* combining the collection of constituents into the completed record.
57+
* <p>
58+
* There is nothing special needed to be done to save a joined record. Once the constituent records are saved
59+
* a joined record instance is implicitly assumed to exist. A subsequent {@link #loadByPrimaryKeyAsync(FDBRecordStore, Tuple, IndexOrphanBehavior)}
60+
* or a query for the record or a scan of a joined index will create the joined record. similarly, a deletion
61+
* of any or all of the constituents will implicitly render the joined record type deleted.
62+
* <p>
63+
* When loading a joined record (and similarly when scanning an index) an {@link IndexOrphanBehavior} can be used
64+
* to determine the behavior in case one or more of the constituents are missing:
65+
* <ul>
66+
* <li>{@link IndexOrphanBehavior#ERROR} (the default) will throw an exception</li>
67+
* <li>{@link IndexOrphanBehavior#RETURN} will return an instance of the records with no constituents</li>
68+
* <li>{@link IndexOrphanBehavior#SKIP} will return null</li>
69+
* </ul>
4570
*/
4671
@API(API.Status.EXPERIMENTAL)
4772
public class JoinedRecordType extends SyntheticRecordType<JoinedRecordType.JoinConstituent> {
@@ -124,10 +149,11 @@ public List<Join> getJoins() {
124149
@Nonnull
125150
@Override
126151
@API(API.Status.INTERNAL)
127-
public CompletableFuture<FDBSyntheticRecord> loadByPrimaryKeyAsync(FDBRecordStore store, Tuple primaryKey) {
152+
public CompletableFuture<FDBSyntheticRecord> loadByPrimaryKeyAsync(FDBRecordStore store, Tuple primaryKey, IndexOrphanBehavior orphanBehavior) {
128153
int nconstituents = getConstituents().size();
129154
final Map<String, FDBStoredRecord<? extends Message>> constituentValues = new ConcurrentHashMap<>(nconstituents);
130155
final CompletableFuture<?>[] futures = new CompletableFuture<?>[nconstituents];
156+
AtomicBoolean isMissingConstituent = new AtomicBoolean(false);
131157
for (int i = 0; i < nconstituents; i++) {
132158
final SyntheticRecordType.Constituent constituent = getConstituents().get(i);
133159
final Tuple constituentKey = primaryKey.getNestedTuple(i + 1);
@@ -136,17 +162,37 @@ public CompletableFuture<FDBSyntheticRecord> loadByPrimaryKeyAsync(FDBRecordStor
136162
} else {
137163
futures[i] = store.loadRecordAsync(constituentKey).thenApply(rec -> {
138164
if (rec == null) {
139-
throw new RecordDoesNotExistException("constituent record not found: " + constituent.getName());
165+
if (orphanBehavior.equals(IndexOrphanBehavior.ERROR)) {
166+
throw new RecordDoesNotExistException("constituent record not found: " + constituent.getName());
167+
} else {
168+
// For SKIP and RETURN
169+
isMissingConstituent.set(true);
170+
// ideally, we should be able to stop the iteration to fetch all other constituents
171+
// but because of the async nature of the loop this seems to be not worth it
172+
}
173+
} else {
174+
constituentValues.put(constituent.getName(), rec);
140175
}
141-
constituentValues.put(constituent.getName(), rec);
142176
return null;
143177
});
144178
}
145179
}
146-
return CompletableFuture.allOf(futures).thenApply(vignore -> FDBSyntheticRecord.of(this, constituentValues));
180+
return CompletableFuture.allOf(futures).thenApply(vignore -> {
181+
if ( ! isMissingConstituent.get()) {
182+
// all constituents have been found
183+
return FDBSyntheticRecord.of(this, constituentValues);
184+
} else {
185+
// some constituents are missing
186+
if (orphanBehavior.equals(IndexOrphanBehavior.SKIP)) {
187+
return null;
188+
} else {
189+
// This is for RETURN - return the shell of the record with no constituents
190+
return FDBSyntheticRecord.of(this, Map.of());
191+
}
192+
}
193+
});
147194
}
148195

149-
150196
@Nonnull
151197
public RecordMetaDataProto.JoinedRecordType toProto() {
152198
RecordMetaDataProto.JoinedRecordType.Builder typeBuilder = RecordMetaDataProto.JoinedRecordType.newBuilder()

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/SyntheticRecordType.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.apple.foundationdb.record.metadata.expressions.KeyExpression;
2626
import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStore;
2727
import com.apple.foundationdb.record.provider.foundationdb.FDBSyntheticRecord;
28+
import com.apple.foundationdb.record.provider.foundationdb.IndexOrphanBehavior;
2829
import com.apple.foundationdb.tuple.Tuple;
2930
import com.google.protobuf.Descriptors;
3031

@@ -89,7 +90,13 @@ public boolean isSynthetic() {
8990

9091
@API(API.Status.INTERNAL)
9192
@Nonnull
92-
public abstract CompletableFuture<FDBSyntheticRecord> loadByPrimaryKeyAsync(FDBRecordStore store, Tuple primaryKey);
93+
public CompletableFuture<FDBSyntheticRecord> loadByPrimaryKeyAsync(FDBRecordStore store, Tuple primaryKey) {
94+
return loadByPrimaryKeyAsync(store, primaryKey, IndexOrphanBehavior.ERROR);
95+
}
96+
97+
@API(API.Status.INTERNAL)
98+
@Nonnull
99+
public abstract CompletableFuture<FDBSyntheticRecord> loadByPrimaryKeyAsync(FDBRecordStore store, Tuple primaryKey, IndexOrphanBehavior orphanBehavior);
93100

94101
@Override
95102
public String toString() {

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/UnnestedRecordType.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStore;
3131
import com.apple.foundationdb.record.provider.foundationdb.FDBStoredRecord;
3232
import com.apple.foundationdb.record.provider.foundationdb.FDBSyntheticRecord;
33+
import com.apple.foundationdb.record.provider.foundationdb.IndexOrphanBehavior;
3334
import com.apple.foundationdb.record.provider.foundationdb.RecordDoesNotExistException;
3435
import com.apple.foundationdb.tuple.Tuple;
3536
import com.google.protobuf.Descriptors;
@@ -262,12 +263,21 @@ public NestedConstituent getParentConstituent() {
262263
@Override
263264
@Nonnull
264265
@API(API.Status.INTERNAL)
265-
public CompletableFuture<FDBSyntheticRecord> loadByPrimaryKeyAsync(@Nonnull final FDBRecordStore store, @Nonnull final Tuple primaryKey) {
266+
public CompletableFuture<FDBSyntheticRecord> loadByPrimaryKeyAsync(@Nonnull final FDBRecordStore store, @Nonnull final Tuple primaryKey, IndexOrphanBehavior orphanBehavior) {
266267
Tuple parentPrimaryKey = primaryKey.getNestedTuple(1);
267268
return store.loadRecordAsync(parentPrimaryKey).thenApply(storedRecord -> {
268269
if (storedRecord == null) {
269-
throw new RecordDoesNotExistException("constituent record not found: " + parentConstituent.getName())
270-
.addLogInfo(LogMessageKeys.PRIMARY_KEY, parentPrimaryKey);
270+
switch (orphanBehavior) {
271+
case ERROR:
272+
throw new RecordDoesNotExistException("constituent record not found: " + parentConstituent.getName())
273+
.addLogInfo(LogMessageKeys.PRIMARY_KEY, parentPrimaryKey);
274+
case SKIP:
275+
return null;
276+
case RETURN:
277+
return FDBSyntheticRecord.of(this, Map.of());
278+
default:
279+
throw new IllegalArgumentException("Unknown orphanBehavior value: " + orphanBehavior);
280+
}
271281
}
272282
Map<String, FDBStoredRecord<?>> constituentValues = new HashMap<>();
273283
constituentValues.put(getParentConstituent().getName(), storedRecord);
@@ -285,8 +295,18 @@ public CompletableFuture<FDBSyntheticRecord> loadByPrimaryKeyAsync(@Nonnull fina
285295
int childConstituentIndex = getConstituents().indexOf(constituent);
286296
int childElemIndex = (int) primaryKey.getNestedTuple(childConstituentIndex + 1).getLong(0);
287297
if (childElemIndex >= childElems.size()) {
288-
throw new RecordCoreException("child element position is too large")
289-
.addLogInfo(LogMessageKeys.CHILD_COUNT, childElems.size());
298+
// Constituent not found
299+
switch (orphanBehavior) {
300+
case ERROR:
301+
throw new RecordCoreException("child element position is too large")
302+
.addLogInfo(LogMessageKeys.CHILD_COUNT, childElems.size());
303+
case SKIP:
304+
return null;
305+
case RETURN:
306+
return FDBSyntheticRecord.of(this, Map.of());
307+
default:
308+
throw new IllegalArgumentException("Unknown orphanBehavior value: " + orphanBehavior);
309+
}
290310
}
291311
Key.Evaluated childElem = childElems.get(childElemIndex);
292312
Message childMessage = childElem.getObject(0, Message.class);

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/FDBRecordStore.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -817,12 +817,12 @@ private CompletableFuture<Void> runSyntheticMaintainers(@Nonnull Map<RecordType,
817817
@API(API.Status.EXPERIMENTAL)
818818
@Nonnull
819819
@Override
820-
public CompletableFuture<FDBSyntheticRecord> loadSyntheticRecord(@Nonnull Tuple primaryKey) {
820+
public CompletableFuture<FDBSyntheticRecord> loadSyntheticRecord(@Nonnull Tuple primaryKey, final IndexOrphanBehavior orphanBehavior) {
821821
SyntheticRecordType<?> syntheticRecordType = getRecordMetaData().getSyntheticRecordTypeFromRecordTypeKey(primaryKey.get(0));
822822
if (syntheticRecordType.getConstituents().size() != primaryKey.size() - 1) {
823823
throw recordCoreException("Primary key does not have correct number of nested keys: " + primaryKey);
824824
}
825-
return syntheticRecordType.loadByPrimaryKeyAsync(this, primaryKey);
825+
return syntheticRecordType.loadByPrimaryKeyAsync(this, primaryKey, orphanBehavior);
826826
}
827827

828828
@Nonnull

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/FDBRecordStoreBase.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,25 @@ default CompletableFuture<FDBStoredRecord<M>> loadRecordAsync(@Nonnull final Tup
720720
*/
721721
@Nonnull
722722
@API(API.Status.EXPERIMENTAL)
723-
CompletableFuture<FDBSyntheticRecord> loadSyntheticRecord(@Nonnull Tuple primaryKey);
723+
default CompletableFuture<FDBSyntheticRecord> loadSyntheticRecord(@Nonnull Tuple primaryKey) {
724+
return loadSyntheticRecord(primaryKey, IndexOrphanBehavior.ERROR);
725+
}
726+
727+
/**
728+
* Load a {@link FDBSyntheticRecord synthetic record} by loading its stored constituent records and synthesizing it from them.
729+
* In case any of the constituents are missing, the following will be returned:
730+
* <ul>
731+
* <li>{@link IndexOrphanBehavior#ERROR}: Exception is thrown</li>
732+
* <li>{@link IndexOrphanBehavior#SKIP}: Future(null) is returned</li>
733+
* <li>{@link IndexOrphanBehavior#RETURN}: Future(Record with no constituents) is returned</li>
734+
* </ul>
735+
* @param primaryKey the primary key of the synthetic record, which includes the primary keys of the constituents
736+
* @param orphanBehavior what to do if any of the record's constituents is missing
737+
* @return a future which completes to the synthesized record
738+
*/
739+
@Nonnull
740+
@API(API.Status.EXPERIMENTAL)
741+
CompletableFuture<FDBSyntheticRecord> loadSyntheticRecord(@Nonnull Tuple primaryKey, IndexOrphanBehavior orphanBehavior);
724742

725743
/**
726744
* Check if a record exists in the record store with the given primary key.

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/FDBTypedRecordStore.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public CompletableFuture<Void> preloadRecordAsync(@Nonnull Tuple primaryKey) {
157157

158158
@Nonnull
159159
@Override
160-
public CompletableFuture<FDBSyntheticRecord> loadSyntheticRecord(@Nonnull final Tuple primaryKey) {
160+
public CompletableFuture<FDBSyntheticRecord> loadSyntheticRecord(@Nonnull final Tuple primaryKey, final IndexOrphanBehavior orphanBehavior) {
161161
throw new RecordCoreException("api unsupported on typed store");
162162
}
163163

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/indexes/ValueIndexScrubbingToolsDangling.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ public class ValueIndexScrubbingToolsDangling implements IndexScrubbingTools<Ind
5757

5858
@Override
5959
public void presetCommonParams(final Index index, final boolean allowRepair, final boolean isSynthetic, final Collection<RecordType> typesIgnored) {
60+
if (isSynthetic && allowRepair) {
61+
throw new UnsupportedOperationException("Scrubbing synthetic records with repair is not supported");
62+
}
6063
this.index = index;
6164
this.allowRepair = allowRepair;
6265
this.isSynthetic = isSynthetic;
@@ -93,7 +96,7 @@ public CompletableFuture<Issue> handleOneItem(final FDBRecordStore store, final
9396
}
9497

9598
if (isSynthetic) {
96-
return store.loadSyntheticRecord(indexEntry.getPrimaryKey()).thenApply(syntheticRecord -> {
99+
return store.loadSyntheticRecord(indexEntry.getPrimaryKey(), IndexOrphanBehavior.RETURN).thenApply(syntheticRecord -> {
97100
if (syntheticRecord.getConstituents().isEmpty()) {
98101
// None of the constituents of this synthetic type are present, so it must be dangling
99102
List<Tuple> primaryKeysForConflict = new ArrayList<>(indexEntry.getPrimaryKey().size() - 1);

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/indexes/ValueIndexScrubbingToolsMissing.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ public class ValueIndexScrubbingToolsMissing implements IndexScrubbingTools<FDBS
6666

6767
@Override
6868
public void presetCommonParams(Index index, boolean allowRepair, boolean isSynthetic, Collection<RecordType> types) {
69+
if (isSynthetic && allowRepair) {
70+
throw new UnsupportedOperationException("Scrubbing synthetic records with repair is not supported");
71+
}
6972
this.recordTypes = types;
7073
this.index = index;
7174
this.allowRepair = allowRepair;

0 commit comments

Comments
 (0)