-
Notifications
You must be signed in to change notification settings - Fork 118
Introduce Serialization format for DataInKeySpacePath #3747
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ced2080
68fd207
67854b9
2c30eb7
fe3b214
76107ba
50a9fed
bc59f62
b155824
066819f
136e9dc
955ba78
4d991b1
f1fb3e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,7 @@ | |
|
|
||
| import javax.annotation.Nonnull; | ||
| import javax.annotation.Nullable; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.function.Function; | ||
|
|
@@ -148,6 +149,18 @@ public DirectoryLayerDirectory(@Nonnull String name, @Nullable Object value, | |
| this.createHooks = createHooks; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isValueValid(@Nullable Object value) { | ||
| // DirectoryLayerDirectory accepts both String (logical names) and Long (directory layer values), | ||
| // but we're making this method stricter, and I hope that using Long is only for a handful of tests, | ||
| // despite comments saying that the resolved value should be allowed. | ||
| if (value instanceof String) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (in which case it may help readability to be explicit about it?) |
||
| // If this directory has a constant value, check that the provided value matches it | ||
| return Objects.equals(getValue(), KeySpaceDirectory.ANY_VALUE) || Objects.equals(getValue(), value); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| @Override | ||
| protected void validateConstant(@Nullable Object value) { | ||
| if (!(value instanceof String)) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -176,6 +176,55 @@ LogMessageKeys.DIR_NAME, getName(), | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Validate that the given value can be used with this directory. | ||
| * <p> | ||
| * Ideally this would be called as part of {@link KeySpacePath#add(String, Object)} to ensure the provided value | ||
| * is valid, but that code has existed for a long time, so it's possible clients are adding without respecting | ||
| * the type. You should call this before calling add to make sure you don't have the same mistakes. At some point | ||
| * this will be embedded in {@code add} once there's some confidence that it won't break anyone's environments. | ||
| * </p> | ||
| * @param value a potential value | ||
| * @throws RecordCoreArgumentException if the value is not valid | ||
| */ | ||
| @API(API.Status.EXPERIMENTAL) | ||
| public void validateValue(@Nullable Object value) { | ||
| // Validate that the value is valid for this directory | ||
| if (!isValueValid(value)) { | ||
| throw new RecordCoreArgumentException("Value does not match directory requirements") | ||
| .addLogInfo(LogMessageKeys.DIR_NAME, name, | ||
| LogMessageKeys.EXPECTED_TYPE, getKeyType(), | ||
| LogMessageKeys.ACTUAL, value, | ||
| "actual_type", value == null ? "null" : value.getClass().getName(), | ||
| "expected_value", getValue()); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Checks if the provided value is valid for this directory. This method can be overridden by subclasses | ||
| * to provide custom validation logic. For example, {@link DirectoryLayerDirectory} accepts String | ||
| * values (logical names) even though its key type is LONG. | ||
| * | ||
| * @param value the value to validate | ||
| * @return {@code true} if the value is valid for this directory | ||
| */ | ||
| @API(API.Status.EXPERIMENTAL) | ||
| public boolean isValueValid(@Nullable Object value) { | ||
| // Check if value matches the key type | ||
| if (!keyType.isMatch(value)) { | ||
| return false; | ||
| } | ||
| // If this directory has a constant value, check that the provided value matches it | ||
| if (this.value != ANY_VALUE) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it guaranteed that in all cases there is only one instance of |
||
| if (this.value instanceof byte[] && value instanceof byte[]) { | ||
| return Arrays.equals((byte[]) this.value, (byte[]) value); | ||
| } else { | ||
| return Objects.equals(this.value, value); | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Given a position in a tuple, checks to see if this directory is compatible with the value at the | ||
| * position, returning either a path indicating that it was compatible or nothing if it was not compatible. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -432,4 +432,13 @@ default RecordCursor<DataInKeySpacePath> exportAllData(@Nonnull FDBRecordContext | |
| @Nonnull | ||
| CompletableFuture<Void> importData(@Nonnull FDBRecordContext context, | ||
| @Nonnull Iterable<DataInKeySpacePath> dataToImport); | ||
|
|
||
| /** | ||
| * Two {@link KeySpacePath}s are equal if they have equal values, the same directory (reference equality) and their | ||
| * parents are the same. | ||
| * @param obj another {@link KeySpacePath} | ||
| * @return {@code true} if this path equals {@code obj} | ||
| */ | ||
| @Override | ||
| boolean equals(Object obj); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the benefit of adding |
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,209 @@ | ||||||
| /* | ||||||
| * KeySpacePathSerializer.java | ||||||
| * | ||||||
| * This source file is part of the FoundationDB open source project | ||||||
| * | ||||||
| * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors | ||||||
| * | ||||||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| * you may not use this file except in compliance with the License. | ||||||
| * You may obtain a copy of the License at | ||||||
| * | ||||||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||||||
| * | ||||||
| * Unless required by applicable law or agreed to in writing, software | ||||||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||||||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
| * See the License for the specific language governing permissions and | ||||||
| * limitations under the License. | ||||||
| */ | ||||||
|
|
||||||
| package com.apple.foundationdb.record.provider.foundationdb.keyspace; | ||||||
|
|
||||||
| import com.apple.foundationdb.annotation.API; | ||||||
| import com.apple.foundationdb.record.RecordCoreArgumentException; | ||||||
| import com.apple.foundationdb.record.logging.LogMessageKeys; | ||||||
| import com.apple.foundationdb.tuple.Tuple; | ||||||
| import com.google.protobuf.ByteString; | ||||||
|
|
||||||
| import javax.annotation.Nonnull; | ||||||
| import javax.annotation.Nullable; | ||||||
| import java.util.List; | ||||||
| import java.util.UUID; | ||||||
|
|
||||||
| /** | ||||||
| * Class for serializing/deserializing between {@link DataInKeySpacePath} and {@link KeySpaceProto.DataInKeySpacePath}. | ||||||
| * <p> | ||||||
| * This will serialize relative to a root path, such that the serialized form is relative to that path. | ||||||
| * <ul> | ||||||
| * <li>This reduces the size of the serialized data, which is particularly important when you have a lot of these.</li> | ||||||
| * <li>This allows the serialized form to be used as an intermediate if you have two identical sub-hierarchies | ||||||
| * in your {@link KeySpace}.</li> | ||||||
| * </ul> | ||||||
| * </p> | ||||||
| * | ||||||
| */ | ||||||
| @API(API.Status.EXPERIMENTAL) | ||||||
| public class KeySpacePathSerializer { | ||||||
|
|
||||||
| @Nonnull | ||||||
| private final List<KeySpacePath> root; | ||||||
|
|
||||||
| /** | ||||||
| * Constructor a new serializer for serializing relative to a given root path. | ||||||
| * @param root a path which is a parent path of all the data you want to serialize | ||||||
| */ | ||||||
| public KeySpacePathSerializer(@Nonnull final KeySpacePath root) { | ||||||
| this.root = root.flatten(); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Serialize the given data relative to the root. | ||||||
| * @param data a piece of data that is contained within the root | ||||||
| * @return the data serialized relative to the root | ||||||
| * @throws RecordCoreArgumentException if the given data is not contained within the root | ||||||
| */ | ||||||
| @Nonnull | ||||||
| public KeySpaceProto.DataInKeySpacePath serialize(@Nonnull DataInKeySpacePath data) { | ||||||
| final List<KeySpacePath> dataPath = data.getPath().flatten(); | ||||||
| // two paths are only equal if their parents are equal, so we don't have to validate the whole prefix here | ||||||
| if (dataPath.size() < root.size() || | ||||||
| !dataPath.get(root.size() - 1).equals(root.get(root.size() - 1))) { | ||||||
| throw new RecordCoreArgumentException("Data is not contained within root path"); | ||||||
| } | ||||||
| KeySpaceProto.DataInKeySpacePath.Builder builder = KeySpaceProto.DataInKeySpacePath.newBuilder(); | ||||||
| for (int i = root.size(); i < dataPath.size(); i++) { | ||||||
| final KeySpacePath keySpacePath = dataPath.get(i); | ||||||
| builder.addPath(serialize(keySpacePath)); | ||||||
| } | ||||||
| if (data.getRemainder() != null) { | ||||||
| builder.setRemainder(ByteString.copyFrom(data.getRemainder().pack())); | ||||||
| } | ||||||
| builder.setValue(ByteString.copyFrom(data.getValue())); | ||||||
| return builder.build(); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Deserialize data relative to the root. | ||||||
| * <p> | ||||||
| * Note: The given data does not need to have come from the same path, but all sub-paths must be valid. | ||||||
| * </p> | ||||||
| * @param proto a serialized form of {@link DataInKeySpacePath} as provided by {@link #serialize(DataInKeySpacePath)} | ||||||
| * @return the deserialized data | ||||||
| * @throws RecordCoreArgumentException if one of the path entries is not valid | ||||||
| * @throws NoSuchDirectoryException if it refers to a directory that doesn't exist within the root | ||||||
| */ | ||||||
| @Nonnull | ||||||
| public DataInKeySpacePath deserialize(@Nonnull KeySpaceProto.DataInKeySpacePath proto) { | ||||||
| // Start with the root path | ||||||
| KeySpacePath path = root.get(root.size() - 1); | ||||||
|
|
||||||
| // Add each path entry from the proto | ||||||
| for (KeySpaceProto.KeySpacePathEntry entry : proto.getPathList()) { | ||||||
| Object value = deserializeValue(entry); | ||||||
| path.getDirectory().getSubdirectory(entry.getName()).validateValue(value); | ||||||
| path = path.add(entry.getName(), value); | ||||||
| } | ||||||
|
|
||||||
| // Extract remainder if present | ||||||
| Tuple remainder = null; | ||||||
| if (proto.hasRemainder()) { | ||||||
| remainder = Tuple.fromBytes(proto.getRemainder().toByteArray()); | ||||||
| } | ||||||
|
|
||||||
| // Extract value | ||||||
| if (!proto.hasValue()) { | ||||||
| throw new RecordCoreArgumentException("Serialized data must have a value"); | ||||||
| } | ||||||
| byte[] value = proto.getValue().toByteArray(); | ||||||
|
|
||||||
| return new DataInKeySpacePath(path, remainder, value); | ||||||
| } | ||||||
|
|
||||||
| @Nullable | ||||||
| private static Object deserializeValue(@Nonnull KeySpaceProto.KeySpacePathEntry entry) { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is the method static? |
||||||
| // Check which value field is set and return the appropriate value | ||||||
| if (entry.hasNullValue()) { | ||||||
| return null; | ||||||
| } else if (entry.hasBytesValue()) { | ||||||
| return entry.getBytesValue().toByteArray(); | ||||||
| } else if (entry.hasStringValue()) { | ||||||
| return entry.getStringValue(); | ||||||
| } else if (entry.hasLongValue()) { | ||||||
| return entry.getLongValue(); | ||||||
| } else if (entry.hasFloatValue()) { | ||||||
| return entry.getFloatValue(); | ||||||
| } else if (entry.hasDoubleValue()) { | ||||||
| return entry.getDoubleValue(); | ||||||
| } else if (entry.hasBooleanValue()) { | ||||||
| return entry.getBooleanValue(); | ||||||
| } else if (entry.hasUuid()) { | ||||||
| KeySpaceProto.KeySpacePathEntry.UUID uuidProto = entry.getUuid(); | ||||||
| return new UUID(uuidProto.getMostSignificantBits(), uuidProto.getLeastSignificantBits()); | ||||||
| } else { | ||||||
| throw new RecordCoreArgumentException("KeySpacePathEntry has no value set") | ||||||
| .addLogInfo(LogMessageKeys.DIR_NAME, entry.getName()); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| @Nonnull | ||||||
| private static KeySpaceProto.KeySpacePathEntry serialize(@Nonnull final KeySpacePath keySpacePath) { | ||||||
| final Object value = keySpacePath.getValue(); | ||||||
| // Use typeOf to get the actual runtime type of the value, rather than the directory's declared keyType. | ||||||
| // This is important for DirectoryLayerDirectory, which has keyType LONG but typically stores String values. | ||||||
| // If we every support something that takes a value that is not supported via KeyType, we'll need to remove this | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| // dependency, but right now it is convenient to reuse that enum. | ||||||
| final KeySpaceDirectory.KeyType keyType = value == null | ||||||
| ? KeySpaceDirectory.KeyType.NULL | ||||||
| : KeySpaceDirectory.KeyType.typeOf(value); | ||||||
|
|
||||||
| KeySpaceProto.KeySpacePathEntry.Builder builder = KeySpaceProto.KeySpacePathEntry.newBuilder() | ||||||
| .setName(keySpacePath.getDirectoryName()); | ||||||
| try { | ||||||
| switch (keyType) { | ||||||
| case NULL: | ||||||
| builder.setNullValue(true); | ||||||
| break; | ||||||
| case BYTES: | ||||||
| builder.setBytesValue(ByteString.copyFrom((byte[])value)); | ||||||
| break; | ||||||
| case STRING: | ||||||
| builder.setStringValue((String)value); | ||||||
| break; | ||||||
| case LONG: | ||||||
| if (value instanceof Integer) { | ||||||
| builder.setLongValue(((Integer)value).longValue()); | ||||||
| } else { | ||||||
| builder.setLongValue((Long)value); | ||||||
| } | ||||||
| break; | ||||||
| case FLOAT: | ||||||
| builder.setFloatValue((Float)value); | ||||||
| break; | ||||||
| case DOUBLE: | ||||||
| builder.setDoubleValue((Double)value); | ||||||
| break; | ||||||
| case BOOLEAN: | ||||||
| builder.setBooleanValue((Boolean)value); | ||||||
| break; | ||||||
| case UUID: | ||||||
| final UUID uuid = (UUID)value; | ||||||
| builder.getUuidBuilder() | ||||||
| .setLeastSignificantBits(uuid.getLeastSignificantBits()) | ||||||
| .setMostSignificantBits(uuid.getMostSignificantBits()); | ||||||
| break; | ||||||
| default: | ||||||
| throw new IllegalStateException("Unexpected value: " + keyType); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } | ||||||
| } catch (ClassCastException e) { | ||||||
| throw new RecordCoreArgumentException("KeySpacePath has incorrect value type", e) | ||||||
| .addLogInfo( | ||||||
| LogMessageKeys.DIR_NAME, keySpacePath.getDirectoryName(), | ||||||
| LogMessageKeys.EXPECTED_TYPE, keyType, | ||||||
| LogMessageKeys.ACTUAL, value); | ||||||
|
|
||||||
| } | ||||||
| return builder.build(); | ||||||
| } | ||||||
|
|
||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| /* | ||
| * keyspace.proto | ||
| * | ||
| * This source file is part of the FoundationDB open source project | ||
| * | ||
| * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| syntax = "proto2"; | ||
|
|
||
| package com.apple.foundationdb.record.provider.foundationdb.keyspace; | ||
| option java_outer_classname = "KeySpaceProto"; | ||
|
|
||
| message DataInKeySpacePath { | ||
| repeated KeySpacePathEntry path = 1; | ||
| optional bytes remainder = 2; | ||
| optional bytes value = 3; | ||
| } | ||
|
|
||
| // Entry representing logical values for a KeySpacePath entry. | ||
| message KeySpacePathEntry { | ||
| optional string name = 1; | ||
|
|
||
| // specific boolean to indicate this is supposed to be a null | ||
| optional bool nullValue = 2; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would |
||
| optional bytes bytesValue = 3; | ||
| optional string stringValue = 4; | ||
| optional int64 longValue = 5; | ||
| optional float floatValue = 6; | ||
| optional double doubleValue = 7; | ||
| optional bool booleanValue = 8; | ||
| optional UUID uuid = 9; | ||
|
|
||
| message UUID { | ||
| // 2 64-bit fields is two tags, the same as 1 bytes field with a length of 16 would be. | ||
| // Using int64 would use more space for a lot of numbers since the uuid is random, it could use up to 10 bytes | ||
| // instead of the fixed 8 bytes (plus the tag). | ||
| // fixed64 would be closer to how these are really used, but would fail the unsigned validator. | ||
| optional sfixed64 most_significant_bits = 1; | ||
| optional sfixed64 least_significant_bits = 2; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a specific reason for prohibiting setting a
LONGhere?