Skip to content

Commit ae1e5f1

Browse files
Transition FormatVersion from constants to an enum (#3304)
The FormatVersion was just an integers, with an associated set of constants, of the form: `FDBRecordStore.*_FORMAT_VERSION`. This PR starts the migration of the code to a new enum: `FormatVersion`, and introduces setters and getters that align with this. This leaves around the existing constants, and methods to avoid being a breaking change, but they are all now flagged with `@API(API.Status.DEPRECATED)` and will be removed in the near future. I did not replace the integer usages in the internals of the record store, because I felt that code was complicated enough that it would be worth focusing on independently. I did change everything else to use the new code. This also adds a bunch of documentation, and thus resolves: #710. I also added a bunch of documentation to the `DatastoreInfo`, because a lot of the details of that are tightly coupled with the FormatVersion. Note: This is somewhat related to #3309 as some of the new paths will also throw an exception on invalid states, whereas the integer versions would not. --------- Co-authored-by: Alec Grieser <[email protected]>
1 parent a5c4a13 commit ae1e5f1

File tree

30 files changed

+828
-251
lines changed

30 files changed

+828
-251
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ public boolean allowsOlderFormerIndexAddedVersions() {
757757
/**
758758
* Whether this validator allows the meta-data to begin to split long records. For record stores that were first
759759
* created with format version
760-
* {@link com.apple.foundationdb.record.provider.foundationdb.FDBRecordStore#SAVE_UNSPLIT_WITH_SUFFIX_FORMAT_VERSION SAVE_UNSPLIT_WITH_SUFFIX_FORMAT_VERSION}
760+
* {@link com.apple.foundationdb.record.provider.foundationdb.FormatVersion#SAVE_UNSPLIT_WITH_SUFFIX FormatVersion.SAVE_UNSPLIT_WITH_SUFFIX}
761761
* or newer, this change is safe as the data layout is the same regardless of the value of
762762
* {@link RecordMetaData#isSplitLongRecords()}. However, older stores used a different format for if records were
763763
* unsplit, and accidentally upgrading the meta-data to split long records is potentially catastrophic as every

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,7 @@ public CompletableFuture<Void> updateStoreRecordVersionsAsync(boolean storeRecor
946946
*
947947
* <p>
948948
* Note that enabling splitting long records could result in data corruption if the record store was initially created with a format version older than
949-
* {@link com.apple.foundationdb.record.provider.foundationdb.FDBRecordStore#SAVE_UNSPLIT_WITH_SUFFIX_FORMAT_VERSION}.
949+
* {@link FormatVersion#SAVE_UNSPLIT_WITH_SUFFIX}.
950950
* Hence the default evolution validator will fail if one enables it. To enable this, first build a custom evolution validator that {@link MetaDataEvolutionValidator#allowsUnsplitToSplit()}
951951
* and use {@link #setEvolutionValidator(MetaDataEvolutionValidator)} to set the evolution validator used by this store.
952952
* For more details, see {@link MetaDataEvolutionValidator#allowsUnsplitToSplit()}.
@@ -961,7 +961,7 @@ public void enableSplitLongRecords() {
961961
*
962962
* <p>
963963
* Note that enabling splitting long records could result in data corruption if the record store was initially created with a format version older than
964-
* {@link com.apple.foundationdb.record.provider.foundationdb.FDBRecordStore#SAVE_UNSPLIT_WITH_SUFFIX_FORMAT_VERSION}.
964+
* {@link FormatVersion#SAVE_UNSPLIT_WITH_SUFFIX}.
965965
* Hence the default evolution validator will fail if one enables it. To enable this, first build a custom evolution validator that {@link MetaDataEvolutionValidator#allowsUnsplitToSplit()}
966966
* and use {@link #setEvolutionValidator(MetaDataEvolutionValidator)} to set the evolution validator used by this store.
967967
* For more details, see {@link MetaDataEvolutionValidator#allowsUnsplitToSplit()}.

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

Lines changed: 91 additions & 55 deletions
Large diffs are not rendered by default.

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

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,8 @@ enum VersionstampSaveBehavior {
464464
*
465465
* <p>
466466
* Note: due to <a href="https://github.com/FoundationDB/fdb-record-layer/issues/964">Issue #964</a>, on some
467-
* older record stores, namely those that were originally created with a {@linkplain FDBRecordStore#getFormatVersion()
468-
* format version} below {@link FDBRecordStore#SAVE_VERSION_WITH_RECORD_FORMAT_VERSION}, records written with a
467+
* older record stores, namely those that were originally created with a {@link FormatVersion} below
468+
* {@link FormatVersion#SAVE_VERSION_WITH_RECORD}, records written with a
469469
* version on stores where {@link com.apple.foundationdb.record.RecordMetaData#isStoreRecordVersions()} is
470470
* {@code false} will not return the version with a record when read, even though the version will be stored.
471471
* Users can avoid this by either migrating data to a new store or by setting {@code isStoreRecordVersions()}
@@ -2158,26 +2158,52 @@ interface BaseBuilder<M extends Message, R extends FDBRecordStoreBase<M>> {
21582158
/**
21592159
* Get the storage format version for this store.
21602160
* @return the format version to use
2161+
* @deprecated use {@link #getFormatVersionEnum()}
21612162
*/
2163+
@Deprecated(forRemoval = true)
21622164
int getFormatVersion();
21632165

2166+
/**
2167+
* Get the {@link FormatVersion} for this store.
2168+
* @return the format version to use
2169+
*/
2170+
default FormatVersion getFormatVersionEnum() {
2171+
return FormatVersion.getFormatVersion(getFormatVersion());
2172+
}
2173+
21642174
/**
21652175
* Set the storage format version for this store.
2166-
*
2167-
* Normally, this should be set to the highest format version supported by all code that may access the record
2168-
* store. {@link #open} will set the store's format version to <code>max(max_supported_version, current_version)</code>.
2169-
* This is to support cases where the target cannot be changed everywhere at once and some instances write the new version before others
2170-
* know that they are licensed to do so. It is still <em>critically</em> important that <em>all</em> instances know how to handle
2171-
* the new version before <em>any</em> instance allows it.
2172-
*
2173-
* When installing a new version of the record layer library that includes a format change, first install everywhere having arranged for
2174-
* {@link #setFormatVersion} to be called with the <em>old</em> format version. Then, after that install is complete, change to the newer version.
21752176
* @param formatVersion the format version to use
21762177
* @return this builder
2178+
* @deprecated This is deprecated, and instead, one should use {@link #setFormatVersion(FormatVersion)}.
21772179
*/
2180+
@Deprecated(forRemoval = true)
21782181
@Nonnull
21792182
BaseBuilder<M, R> setFormatVersion(int formatVersion);
21802183

2184+
/**
2185+
* Set the storage format version for this store.
2186+
* <p>
2187+
* Normally, this should be set to the highest format version supported by all code that may access the
2188+
* record store. {@link #open} will set the store's format version to the greater of what is provided here
2189+
* and the one currently set on the store.
2190+
* This is to support cases where the target cannot be changed everywhere at once and some instances write
2191+
* the new version before others know that they are licensed to do so. It is still <em>critically</em>
2192+
* important that <em>all</em> instances know how to handle the new version before <em>any</em> instance
2193+
* allows it.
2194+
* </p>
2195+
* <p>
2196+
* When installing a new version of the record layer library that includes a format change, first install
2197+
* everywhere having arranged for {@link #setFormatVersion} to be called with the <em>old</em> format
2198+
* version. Then, after that install is complete, change to the newer version.
2199+
* </p>
2200+
* @param formatVersion the format version to use
2201+
* @return this builder
2202+
*/
2203+
default BaseBuilder<M, R> setFormatVersion(FormatVersion formatVersion) {
2204+
return setFormatVersion(formatVersion.getValueForSerialization());
2205+
}
2206+
21812207
/**
21822208
* Get the provider for the record store's meta-data.
21832209
* @return the meta-data source to use

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public interface FDBStoredSizes {
6161
* In particular, this states whether there was a version stored
6262
* directly with the record in the database, which should only be
6363
* true if the format version of the database is greater than or
64-
* equal to {@link FDBRecordStore#SAVE_VERSION_WITH_RECORD_FORMAT_VERSION}.
64+
* equal to {@link FormatVersion#SAVE_VERSION_WITH_RECORD}.
6565
* @return {@code true} if this record is stored with a version in-line
6666
*/
6767
boolean isVersionedInline();

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,17 +384,32 @@ public Builder<M> setUntypedSerializer(@Nonnull RecordSerializer<Message> serial
384384
}
385385

386386
@Override
387+
@Deprecated(forRemoval = true)
388+
@SuppressWarnings("removal") // this method is deprecated to be removed with parent
387389
public int getFormatVersion() {
388390
return untypedStoreBuilder.getFormatVersion();
389391
}
390392

391-
@Nonnull
392393
@Override
394+
public FormatVersion getFormatVersionEnum() {
395+
return untypedStoreBuilder.getFormatVersionEnum();
396+
}
397+
398+
@Override
399+
@Nonnull
400+
@Deprecated(forRemoval = true)
401+
@SuppressWarnings("removal") // this method is deprecated to be removed with parent
393402
public Builder<M> setFormatVersion(int formatVersion) {
394403
untypedStoreBuilder.setFormatVersion(formatVersion);
395404
return this;
396405
}
397406

407+
@Override
408+
public BaseBuilder<M, FDBTypedRecordStore<M>> setFormatVersion(final FormatVersion formatVersion) {
409+
untypedStoreBuilder.setFormatVersion(formatVersion);
410+
return this;
411+
}
412+
398413
@Nullable
399414
@Override
400415
public RecordMetaDataProvider getMetaDataProvider() {
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
/*
2+
* FormatVersion.java
3+
*
4+
* This source file is part of the FoundationDB open source project
5+
*
6+
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package com.apple.foundationdb.record.provider.foundationdb;
22+
23+
import com.apple.foundationdb.annotation.API;
24+
25+
import javax.annotation.Nonnull;
26+
import java.util.Arrays;
27+
import java.util.Comparator;
28+
import java.util.Map;
29+
import java.util.stream.Collectors;
30+
31+
/**
32+
* This version is recorded for each store, and controls certain aspects of the on-disk behavior.
33+
* <p>
34+
* The primary reason for this version is to ensure that if two different versions of code interact with the same
35+
* store, the old version won't misinterpret other data in the store. Other than {@link #FORMAT_CONTROL} all of
36+
* these can result in a server misunderstanding, and consequently incorrectly updating data in the store.
37+
* </p>
38+
* <p>
39+
* When the store is opened, the format version on disk will be upgraded to the one provided by
40+
* {@link FDBRecordStore.Builder#setFormatVersion}, or {@link #getDefaultFormatVersion()}. There is not currently
41+
* a defined policy for increasing the default version, so it is best to set the format version explicitly; see
42+
* <a href="https://github.com/FoundationDB/fdb-record-layer/issues/709">issue #709</a> for more information.
43+
* </p>
44+
* <p>
45+
* Generally, if all running instances support a given format version, it is ok to start using it, however upgrading
46+
* some format versions may be expensive, especially older versions on larger stores.
47+
* </p>
48+
*/
49+
@API(API.Status.UNSTABLE)
50+
public enum FormatVersion implements Comparable<FormatVersion> {
51+
/**
52+
* Initial FormatVersion.
53+
*/
54+
INFO_ADDED(1),
55+
/**
56+
* This FormatVersion introduces support for tracking record conuts as defined by:
57+
* {@link com.apple.foundationdb.record.RecordMetaData#getRecordCountKey()}.
58+
*/
59+
RECORD_COUNT_ADDED(2),
60+
/**
61+
* This FormatVersion causes the key as defined in {@link com.apple.foundationdb.record.RecordMetaData#getRecordCountKey()} to be stored in the
62+
* StoreHeader, ensuring that if the key is changed, the record count will be updated.
63+
* <p>
64+
* Unlike indexes, the RecordCountKey does not have a {@code lastModifiedVersion}, and thus the store detects
65+
* that the counts need to be rebuilt by checking the key in the StoreHeader.
66+
* </p>
67+
* <p>
68+
* Warning: There is no way to rebuild the record count key across transactions, and it does not check the
69+
* {@link com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase.UserVersionChecker}, so changing the record count key will cause the store to attempt to rebuild the
70+
* counts when opening the store. If you have not started using this version (or
71+
* {@link #RECORD_COUNT_ADDED}), you may want to consider replacing the RecordCountKey with a
72+
* {@link com.apple.foundationdb.record.metadata.IndexTypes#COUNT} index first.
73+
* </p>
74+
*/
75+
RECORD_COUNT_KEY_ADDED(3),
76+
/**
77+
* This FormatVersion was introduced to support testing of upgrading the format version past
78+
* {@link #RECORD_COUNT_KEY_ADDED}, but does not change the behavior of the store.
79+
*/
80+
FORMAT_CONTROL(4),
81+
/**
82+
* This FormatVersion causes all stores to store the split suffix, even if
83+
* {@link com.apple.foundationdb.record.RecordMetaData#isSplitLongRecords()} is {@code false}, unless
84+
* {@link com.apple.foundationdb.record.RecordMetaDataProto.DataStoreInfo#getOmitUnsplitRecordSuffix()} is {@code true} on the StoreHeader.
85+
* <p>
86+
* In order to maintain backwards compatibility, and not require rewriting all the records, if upgrading from an
87+
* earlier FormatVersion to this one, if the metadata does not allow splitting long records,
88+
* {@linkplain com.apple.foundationdb.record.RecordMetaDataProto.DataStoreInfo#getOmitUnsplitRecordSuffix() getOmitUnsplitRecordSuffix()}
89+
* will be set to {@code true} on the StoreHeader.
90+
* </p>
91+
* <p>
92+
* By always including the suffix, it allows a couple benefits:
93+
* <ul>
94+
* <li>The metadata will be able to change to support splitting long records in the future</li>
95+
* <li>When upgrading to {@link #SAVE_UNSPLIT_WITH_SUFFIX} it can store the versions adjacent
96+
* to the record, rather than a separate sub-range.</li>
97+
* </ul>
98+
* </p>
99+
*/
100+
SAVE_UNSPLIT_WITH_SUFFIX(5),
101+
/**
102+
* This FormatVersion causes the record versions (if enabled via {@link com.apple.foundationdb.record.RecordMetaData#isStoreRecordVersions()})
103+
* to be stored adjacent to the record itself, rather than in a separate sub-range.
104+
* <p>
105+
* This most notably improves the performance or load of reading records, particularly a range of records, as it
106+
* doesn't have to do a separate read to get the versions.
107+
* </p>
108+
* <p>
109+
* Note: If the store is omitting the unsplit record suffix due to
110+
* {@link com.apple.foundationdb.record.RecordMetaDataProto.DataStoreInfo#getOmitUnsplitRecordSuffix()}, this will continue to store the
111+
* record versions in the separate space.
112+
* </p>
113+
* <p>
114+
* Warning: If {@link com.apple.foundationdb.record.RecordMetaData#isStoreRecordVersions()} is enabled when upgrading to this version,
115+
* the code will try to move the versions transactionally when opening the store.
116+
* </p>
117+
*/
118+
SAVE_VERSION_WITH_RECORD(6),
119+
/**
120+
* This FormatVersion allows the record state to be cached and invalidated with the meta-data version key.
121+
* <p>
122+
* With this version, every time the {@link com.apple.foundationdb.record.RecordStoreState}
123+
* (storeHeader and index states) is updated for a store marked as cacheable, the meta-data-version for the cluster will be bumped,
124+
* and all cache entries for all stores in the cluster are invalidated. Consequently if a store did not know
125+
* that a store was cacheable, when it was, it could update the store header or index state, without
126+
* invalidating the cache, causing others to interact with the store using an old version of the metadata, or
127+
* treating an index as disabled.
128+
* </p>
129+
* @see com.apple.foundationdb.record.provider.foundationdb.storestate.MetaDataVersionStampStoreStateCache
130+
* @see FDBRecordStore#setStateCacheability
131+
*/
132+
CACHEABLE_STATE(7),
133+
/**
134+
* This FormatVersion allows the user to store additional fields in the StoreHeader.
135+
* These fields aren't used by the record store itself, but allow the user to set and read additional information.
136+
* @see FDBRecordStore#setHeaderUserField
137+
*
138+
*/
139+
HEADER_USER_FIELDS(8),
140+
/**
141+
* This FormatVersion allows the store to mark indexes as {@link com.apple.foundationdb.record.IndexState#READABLE_UNIQUE_PENDING} if
142+
* appropriate.
143+
*/
144+
READABLE_UNIQUE_PENDING(9),
145+
/**
146+
* This FormatVersion allows building non-idempotent indexes (e.g. COUNT) from a source index.
147+
*/
148+
CHECK_INDEX_BUILD_TYPE_DURING_UPDATE(10);
149+
150+
private final int value;
151+
152+
private static final Map<Integer, FormatVersion> VERSIONS = Arrays.stream(values())
153+
.collect(Collectors.toUnmodifiableMap(FormatVersion::getValueForSerialization,
154+
version -> version));
155+
156+
private static final FormatVersion MAX_SUPPORTED_VERSION = Arrays.stream(values())
157+
.max(Comparator.naturalOrder())
158+
.orElseThrow(); // will throw if there are on enum values
159+
160+
161+
FormatVersion(final int value) {
162+
this.value = value;
163+
}
164+
165+
/**
166+
* The minimum {@code FormatVersion}.
167+
* @return the minimum {@code FormatVersion}
168+
*/
169+
static FormatVersion getMinimumVersion() {
170+
return INFO_ADDED;
171+
}
172+
173+
/**
174+
* The maximum {@code FormatVersion} that this version of the Record Layer can support.
175+
* @return the maximum supported version
176+
*/
177+
public static FormatVersion getMaximumSupportedVersion() {
178+
return MAX_SUPPORTED_VERSION;
179+
}
180+
181+
/**
182+
* The default FormatVersion that this code will set when opening a record store, if the user does not call
183+
* {@link FDBRecordStore.Builder#setFormatVersion}.
184+
* <p>
185+
* Note: We don't currently have a well-defined policy for updating this, see
186+
* <a href="https://github.com/FoundationDB/fdb-record-layer/issues/709">Issue #709</a>.
187+
* </p>
188+
*/
189+
public static FormatVersion getDefaultFormatVersion() {
190+
return CACHEABLE_STATE;
191+
}
192+
193+
@API(API.Status.INTERNAL)
194+
int getValueForSerialization() {
195+
return value;
196+
}
197+
198+
@API(API.Status.INTERNAL)
199+
static void validateFormatVersion(int candidateVersion, final SubspaceProvider subspaceProvider) {
200+
if (candidateVersion < getMinimumVersion().getValueForSerialization() ||
201+
candidateVersion > getMaximumSupportedVersion().getValueForSerialization()) {
202+
throw new UnsupportedFormatVersionException("Unsupported format version " + candidateVersion,
203+
subspaceProvider.logKey(), subspaceProvider);
204+
}
205+
}
206+
207+
@Nonnull
208+
@API(API.Status.INTERNAL)
209+
public static FormatVersion getFormatVersion(int candidateVersion) {
210+
final FormatVersion candidate = VERSIONS.get(candidateVersion);
211+
if (candidate == null) {
212+
throw new UnsupportedFormatVersionException("Unsupported format version " + candidateVersion);
213+
}
214+
return candidate;
215+
}
216+
217+
/**
218+
* Returns whether this version is {@code >=} the provided version.
219+
* @param other a different format version.
220+
* @return {@code true} if this format version is the same as {@code other} or greater than it
221+
*/
222+
public boolean isAtLeast(final FormatVersion other) {
223+
return this.compareTo(other) >= 0;
224+
}
225+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ private void validateIdempotenceIfNecessary(@Nonnull FDBRecordStore store, @Nonn
260260
// had been built already by looking for its primary key in the range set, which is incorrect for most source
261261
// indexes. After this format version, we are assured that all updates will check the index type first and
262262
// respond appropriately
263-
if (store.getFormatVersion() < FDBRecordStore.CHECK_INDEX_BUILD_TYPE_DURING_UPDATE_FORMAT_VERSION) {
263+
if (!store.getFormatVersionEnum().isAtLeast(FormatVersion.CHECK_INDEX_BUILD_TYPE_DURING_UPDATE)) {
264264
validateOrThrowEx(maintainer.isIdempotent(), "target index is not idempotent");
265265
}
266266
}

0 commit comments

Comments
 (0)