This is an automated email from the ASF dual-hosted git repository. sdanilov pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push: new 154447cf6 IGNITE-16921 Support schema changes concerning inheritance hierarchy (#802) 154447cf6 is described below commit 154447cf6c202b22b11ed98c0191e2f069ebd7c4 Author: Roman Puchkovskiy <roman.puchkovs...@gmail.com> AuthorDate: Thu May 12 18:03:57 2022 +0400 IGNITE-16921 Support schema changes concerning inheritance hierarchy (#802) --- .../network/serialization/BrokenFieldAccessor.java | 11 +- .../serialization/BrokenSerializationMethods.java | 57 +++++ .../network/serialization/ClassDescriptor.java | 92 +++++++- .../serialization/ClassDescriptorMerger.java | 57 +++++ ...ClassNameMapBackedClassIndexedDescriptors.java} | 10 +- .../network/serialization/FieldAccessor.java | 2 +- .../network/serialization/FieldDescriptor.java | 41 +++- .../network/serialization/MergedLayer.java | 99 +++++++++ .../PerSessionSerializationService.java | 141 ++++++++---- .../serialization/SerializationException.java | 4 +- .../marshal/DefaultUserObjectMarshaller.java | 13 +- .../marshal/SchemaMismatchEventSource.java | 11 + .../marshal/SchemaMismatchHandlers.java | 36 +-- .../marshal/StructuredObjectMarshaller.java | 54 +++-- .../serialization/ClassDescriptorMergerTest.java | 181 +++++++++++++++ ...sNameMapBackedClassIndexedDescriptorsTest.java} | 8 +- ...ltUserObjectMarshallerWithSchemaChangeTest.java | 242 ++++++++++++++++++++- 17 files changed, 932 insertions(+), 127 deletions(-) diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BrokenFieldAccessor.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BrokenFieldAccessor.java index 1cf9c6575..41e766d35 100644 --- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BrokenFieldAccessor.java +++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BrokenFieldAccessor.java @@ -22,11 +22,11 @@ package org.apache.ignite.internal.network.serialization; */ class BrokenFieldAccessor implements FieldAccessor { private final String fieldName; - private final Class<?> declaringClass; + private final String declaringClassName; - BrokenFieldAccessor(String fieldName, Class<?> declaringClass) { + BrokenFieldAccessor(String fieldName, String declaringClassName) { this.fieldName = fieldName; - this.declaringClass = declaringClass; + this.declaringClassName = declaringClassName; } /** {@inheritDoc} */ @@ -137,9 +137,8 @@ class BrokenFieldAccessor implements FieldAccessor { throw brokenException(); } - private RuntimeException brokenException() { - return new IllegalStateException("Field " + declaringClass.getName() + "#" + fieldName + private IllegalStateException brokenException() { + return new IllegalStateException("Field " + declaringClassName + "#" + fieldName + " is missing locally, no accesses to it should be performed, this is a bug"); } - } diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BrokenSerializationMethods.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BrokenSerializationMethods.java new file mode 100644 index 000000000..c1b64adce --- /dev/null +++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BrokenSerializationMethods.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.ignite.internal.network.serialization; + +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +/** + * {@link SpecialSerializationMethods} implementation that always throws. + */ +class BrokenSerializationMethods implements SpecialSerializationMethods { + private final String missingClassName; + + BrokenSerializationMethods(String missingClassName) { + this.missingClassName = missingClassName; + } + + @Override + public Object writeReplace(Object object) throws SpecialMethodInvocationException { + throw brokenException(); + } + + @Override + public Object readResolve(Object object) throws SpecialMethodInvocationException { + throw brokenException(); + } + + @Override + public void writeObject(Object object, ObjectOutputStream stream) throws SpecialMethodInvocationException { + throw brokenException(); + } + + @Override + public void readObject(Object object, ObjectInputStream stream) throws SpecialMethodInvocationException { + throw brokenException(); + } + + private IllegalStateException brokenException() { + return new IllegalStateException("Class " + missingClassName + + " is missing locally, no accesses to it should be performed, this is a bug"); + } +} diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptor.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptor.java index c108beaee..8f9360faa 100644 --- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptor.java +++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptor.java @@ -35,10 +35,17 @@ import org.jetbrains.annotations.Nullable; * Class descriptor for the user object serialization. */ public class ClassDescriptor implements DeclaredType { + /** + * Class name. + */ + private final String className; + /** * Class. It is local to the current JVM; the descriptor could * be created on a remote JVM/machine where a class with same name could represent a different class. + * Might be absent in a descriptor describing a remote class that is not present locally. */ + @Nullable private final Class<?> localClass; /** @@ -116,6 +123,8 @@ public class ClassDescriptor implements DeclaredType { private final List<ClassDescriptor> lineage; + private final List<MergedLayer> mergedLineage; + private final SpecialSerializationMethods serializationMethods; /** @@ -133,7 +142,7 @@ public class ClassDescriptor implements DeclaredType { } /** - * Creates a descriptor describing a remote class. + * Creates a descriptor describing a remote class (when a local class corresponding to the remote class exists). */ public static ClassDescriptor forRemote( Class<?> localClass, @@ -151,6 +160,7 @@ public class ClassDescriptor implements DeclaredType { Objects.requireNonNull(localDescriptor); return new ClassDescriptor( + localClass.getName(), localClass, descriptorId, superClassDescriptor, @@ -162,10 +172,44 @@ public class ClassDescriptor implements DeclaredType { fields, serialization, ClassDescriptorMerger.mergeFields(localDescriptor.fields(), fields), + false, localDescriptor ); } + /** + * Creates a descriptor describing a remote class (when no local class corresponding to the remote class exists). + */ + public static ClassDescriptor forRemote( + String className, + int descriptorId, + @Nullable ClassDescriptor superClassDescriptor, + @Nullable ClassDescriptor componentTypeDescriptor, + boolean isPrimitive, + boolean isArray, + boolean isRuntimeEnum, + boolean isRuntimeTypeKnownUpfront, + List<FieldDescriptor> fields, + Serialization serialization + ) { + return new ClassDescriptor( + className, + null, + descriptorId, + superClassDescriptor, + componentTypeDescriptor, + isPrimitive, + isArray, + isRuntimeEnum, + isRuntimeTypeKnownUpfront, + fields, + serialization, + fields.stream().map(MergedField::remoteOnly).collect(toList()), + false, + null + ); + } + /** * Constructor for the local class case. */ @@ -178,6 +222,7 @@ public class ClassDescriptor implements DeclaredType { Serialization serialization ) { this( + localClass.getName(), localClass, descriptorId, superClassDescriptor, @@ -189,6 +234,7 @@ public class ClassDescriptor implements DeclaredType { fields, serialization, fields.stream().map(field -> new MergedField(field, field)).collect(toList()), + true, null ); } @@ -197,7 +243,8 @@ public class ClassDescriptor implements DeclaredType { * Constructor. */ private ClassDescriptor( - Class<?> localClass, + String className, + @Nullable Class<?> localClass, int descriptorId, @Nullable ClassDescriptor superClassDescriptor, @Nullable ClassDescriptor componentTypeDescriptor, @@ -208,8 +255,13 @@ public class ClassDescriptor implements DeclaredType { List<FieldDescriptor> fields, Serialization serialization, List<MergedField> mergedFields, + boolean thisIsLocal, @Nullable ClassDescriptor localDescriptor ) { + assert localClass != null && (thisIsLocal || localDescriptor != null) + || (localClass == null && !thisIsLocal && localDescriptor == null); + + this.className = className; this.localClass = localClass; this.descriptorId = descriptorId; this.superClassDescriptor = superClassDescriptor; @@ -222,7 +274,7 @@ public class ClassDescriptor implements DeclaredType { this.fields = List.copyOf(fields); this.serialization = serialization; - this.localDescriptor = localDescriptor == null ? this : localDescriptor; + this.localDescriptor = thisIsLocal ? this : localDescriptor; this.mergedFields = List.copyOf(mergedFields); @@ -233,8 +285,16 @@ public class ClassDescriptor implements DeclaredType { fieldNullsBitmapIndices = computeFieldNullsBitmapIndices(fields); lineage = computeLineage(this); + if (thisIsLocal) { + mergedLineage = lineage.stream().map(layer -> new MergedLayer(layer, layer)).collect(toList()); + } else if (localDescriptor == null) { + mergedLineage = lineage.stream().map(MergedLayer::remoteOnly).collect(toList()); + } else { + mergedLineage = ClassDescriptorMerger.mergeLineages(localDescriptor.lineage(), this.lineage); + } - serializationMethods = new SpecialSerializationMethodsImpl(this); + serializationMethods = localClass != null ? new SpecialSerializationMethodsImpl(this) + : new BrokenSerializationMethods(className); } private static int computePrimitiveFieldsDataSize(List<FieldDescriptor> fields) { @@ -388,16 +448,21 @@ public class ClassDescriptor implements DeclaredType { * @return Class' name. */ public String className() { - return localClass.getName(); + return className; } /** * Returns descriptor's class (represented by a local class). Local means 'on this machine', but the descriptor could * be created on a remote machine where a class with same name could represent a different class. * - * @return Class. + * @return local class + * @throws IllegalStateException if no local class exists for the described class (this could happen for a remote descriptor) */ public Class<?> localClass() { + if (localClass == null) { + throw new IllegalStateException("No local class exists for '" + className + "'"); + } + return localClass; } @@ -558,14 +623,23 @@ public class ClassDescriptor implements DeclaredType { return descriptorId == BuiltInType.PROXY.descriptorId(); } + public boolean hasLocal() { + return localDescriptor != null; + } + /** * Returns descriptor of the local version of the remote class described by this descriptor * (or {@code null} if the current descriptor describes a local class). * * @return descriptor of the local version of the remote class described by this descriptor * (or {@code null} if the current descriptor describes a local class) + * @throws IllegalStateException if no local counterpart exists for the described class (this could happen for a remote descriptor) */ public ClassDescriptor local() { + if (localDescriptor == null) { + throw new IllegalStateException("No local descriptor exists for '" + className + "'"); + } + return localDescriptor; } @@ -585,7 +659,7 @@ public class ClassDescriptor implements DeclaredType { * @return {@code true} if this descriptor describes same class as the given descriptor */ public boolean describesSameClass(ClassDescriptor other) { - return localClass == other.localClass; + return Objects.equals(className, other.className); } /** @@ -799,6 +873,10 @@ public class ClassDescriptor implements DeclaredType { return lineage; } + public List<MergedLayer> mergedLineage() { + return mergedLineage; + } + /** * Returns {@code true} if the descriptor describes a String that is represented with Latin-1 internally. * Needed to apply an optimization. diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorMerger.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorMerger.java index 93d743fdd..a4da38ed1 100644 --- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorMerger.java +++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorMerger.java @@ -17,8 +17,12 @@ package org.apache.ignite.internal.network.serialization; +import static java.util.stream.Collectors.toSet; + import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.function.Predicate; /** * Merges {@link ClassDescriptor} components. @@ -69,4 +73,57 @@ class ClassDescriptorMerger { return List.copyOf(mergedFields); } + + static List<MergedLayer> mergeLineages(List<ClassDescriptor> localLineage, List<ClassDescriptor> remoteLineage) { + Set<String> commonClassNames = classNames(localLineage); + commonClassNames.retainAll(classNames(remoteLineage)); + + Predicate<ClassDescriptor> isCommon = layer -> commonClassNames.contains(layer.className()); + + List<MergedLayer> result = new ArrayList<>(); + + int localIndex = 0; + int remoteIndex = 0; + + while (localIndex < localLineage.size() && remoteIndex < remoteLineage.size()) { + while (localIndex < localLineage.size() && !isCommon.test(localLineage.get(localIndex))) { + result.add(MergedLayer.localOnly(localLineage.get(localIndex))); + localIndex++; + } + while (remoteIndex < remoteLineage.size() && !isCommon.test(remoteLineage.get(remoteIndex))) { + result.add(MergedLayer.remoteOnly(remoteLineage.get(remoteIndex))); + remoteIndex++; + } + + if (localIndex >= localLineage.size() || remoteIndex >= remoteLineage.size()) { + break; + } + + // in both lists, we are standing on descriptors with same class name + ClassDescriptor localLayer = localLineage.get(localIndex); + ClassDescriptor remoteLayer = remoteLineage.get(remoteIndex); + result.add(new MergedLayer(localLayer, remoteLayer)); + + localIndex++; + remoteIndex++; + } + + // tails + while (localIndex < localLineage.size()) { + result.add(MergedLayer.localOnly(localLineage.get(localIndex))); + localIndex++; + } + while (remoteIndex < remoteLineage.size()) { + result.add(MergedLayer.remoteOnly(remoteLineage.get(remoteIndex))); + remoteIndex++; + } + + return List.copyOf(result); + } + + private static Set<String> classNames(List<ClassDescriptor> localLineage) { + return localLineage.stream() + .map(ClassDescriptor::className) + .collect(toSet()); + } } diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/MapBackedClassIndexedDescriptors.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassNameMapBackedClassIndexedDescriptors.java similarity index 76% rename from modules/network/src/main/java/org/apache/ignite/internal/network/serialization/MapBackedClassIndexedDescriptors.java rename to modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassNameMapBackedClassIndexedDescriptors.java index 20ddcef8b..096a97b6b 100644 --- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/MapBackedClassIndexedDescriptors.java +++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassNameMapBackedClassIndexedDescriptors.java @@ -21,12 +21,12 @@ import java.util.Map; import org.jetbrains.annotations.Nullable; /** - * A map-backed implementation of {@link ClassIndexedDescriptors}. + * A map-backed implementation of {@link ClassIndexedDescriptors}, map stores class names as keys. */ -public class MapBackedClassIndexedDescriptors implements ClassIndexedDescriptors { - private final Map<Class<?>, ClassDescriptor> descriptorsByClass; +public class ClassNameMapBackedClassIndexedDescriptors implements ClassIndexedDescriptors { + private final Map<String, ClassDescriptor> descriptorsByClass; - public MapBackedClassIndexedDescriptors(Map<Class<?>, ClassDescriptor> descriptorsByClass) { + public ClassNameMapBackedClassIndexedDescriptors(Map<String, ClassDescriptor> descriptorsByClass) { this.descriptorsByClass = descriptorsByClass; } @@ -34,6 +34,6 @@ public class MapBackedClassIndexedDescriptors implements ClassIndexedDescriptors @Override @Nullable public ClassDescriptor getDescriptor(Class<?> clazz) { - return descriptorsByClass.get(clazz); + return descriptorsByClass.get(clazz.getName()); } } diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldAccessor.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldAccessor.java index 40717dc5d..5fdfe5328 100644 --- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldAccessor.java +++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldAccessor.java @@ -48,7 +48,7 @@ public interface FieldAccessor { if (field != null) { return new UnsafeFieldAccessor(field); } else { - return new BrokenFieldAccessor(fieldName, declaringClass); + return new BrokenFieldAccessor(fieldName, declaringClass.getName()); } } diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldDescriptor.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldDescriptor.java index ef9cb0f21..335e60fcc 100644 --- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldDescriptor.java +++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldDescriptor.java @@ -30,7 +30,7 @@ public class FieldDescriptor implements DeclaredType { /** * Type of the field (represented by a local class). Local means 'on this machine', but the descriptor could - * be created on a remote machine where a class with same name could represent a different class.. + * be created on a remote machine where a class with same name could represent a different class. */ private final Class<?> localClass; @@ -56,7 +56,15 @@ public class FieldDescriptor implements DeclaredType { * Creates a {@link FieldDescriptor} from a local {@link Field}. */ public static FieldDescriptor local(Field field, int typeDescriptorId) { - return new FieldDescriptor(field, typeDescriptorId); + return new FieldDescriptor( + field.getName(), + field.getType(), + typeDescriptorId, + false, + field.getType().isPrimitive(), + Classes.isRuntimeTypeKnownUpfront(field.getType()), + FieldAccessor.forField(field) + ); } /** @@ -80,7 +88,7 @@ public class FieldDescriptor implements DeclaredType { } /** - * Creates a {@link FieldDescriptor} for a remote field. + * Creates a {@link FieldDescriptor} for a remote field when the defining class is present locally. */ public static FieldDescriptor remote( String fieldName, @@ -102,17 +110,26 @@ public class FieldDescriptor implements DeclaredType { } /** - * Constructor. + * Creates a {@link FieldDescriptor} for a remote field when the defining class is not present locally. + * The resulting FieldDescriptor cannot be used for accessing the field using {@link #accessor()} (because the + * returned {@link FieldAccessor} will throw on any access attempt). */ - private FieldDescriptor(Field field, int typeDescriptorId) { - this( - field.getName(), - field.getType(), + public static FieldDescriptor remote( + String fieldName, + Class<?> fieldClazz, + int typeDescriptorId, + boolean unshared, + boolean isPrimitive, + boolean isRuntimeTypeKnownUpfront, + String declaringClassName) { + return new FieldDescriptor( + fieldName, + fieldClazz, typeDescriptorId, - false, - field.getType().isPrimitive(), - Classes.isRuntimeTypeKnownUpfront(field.getType()), - FieldAccessor.forField(field) + unshared, + isPrimitive, + isRuntimeTypeKnownUpfront, + new BrokenFieldAccessor(fieldName, declaringClassName) ); } diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/MergedLayer.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/MergedLayer.java new file mode 100644 index 000000000..313f34827 --- /dev/null +++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/MergedLayer.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.ignite.internal.network.serialization; + +import java.util.Objects; +import org.jetbrains.annotations.Nullable; + +/** + * Contains information about a class layer, both from local and remote classes. Any of the parts (local/remote) might be absent. + */ +public class MergedLayer { + @Nullable + private final ClassDescriptor localLayer; + @Nullable + private final ClassDescriptor remoteLayer; + + /** + * Creates an instance with local layer only. + * + * @param localLayer the local layer + * @return an instance with local layer only + */ + public static MergedLayer localOnly(ClassDescriptor localLayer) { + return new MergedLayer(localLayer, null); + } + + /** + * Creates an instance with remote layer only. + * + * @param remoteLayer the remote layer + * @return an instance with remote layer only + */ + public static MergedLayer remoteOnly(ClassDescriptor remoteLayer) { + return new MergedLayer(null, remoteLayer); + } + + /** + * Constructs a new instance. + */ + public MergedLayer(@Nullable ClassDescriptor localLayer, @Nullable ClassDescriptor remoteLayer) { + assert localLayer != null || remoteLayer != null : "Both descriptors are null"; + assert localLayer == null || remoteLayer == null || Objects.equals(localLayer.className(), remoteLayer.className()) + : "Descriptors of different classes: " + localLayer.className() + " and " + remoteLayer.className(); + + this.localLayer = localLayer; + this.remoteLayer = remoteLayer; + } + + /** + * Returns {@code true} if local layer is present. + * + * @return {@code true} if local layer is present + */ + public boolean hasLocal() { + return localLayer != null; + } + + /** + * Returns local layer. + * + * @return local layer + */ + public ClassDescriptor local() { + return Objects.requireNonNull(localLayer, "localLayer is null"); + } + + /** + * Returns {@code true} if remote layer is present. + * + * @return {@code true} if remote layer is present + */ + public boolean hasRemote() { + return remoteLayer != null; + } + + /** + * Returns remote layer. + * + * @return remote layer + */ + public ClassDescriptor remote() { + return Objects.requireNonNull(remoteLayer, "remoteLayer is null"); + } +} diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/PerSessionSerializationService.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/PerSessionSerializationService.java index d1ba3f12f..5a1eb0967 100644 --- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/PerSessionSerializationService.java +++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/PerSessionSerializationService.java @@ -61,9 +61,9 @@ public class PerSessionSerializationService { private final Int2ObjectMap<ClassDescriptor> mergedIdToDescriptorMap = new Int2ObjectOpenHashMap<>(); /** * Map with merged class descriptors. They are the result of the merging of a local and a remote descriptor. - * The key in this map is the class. + * The key in this map is the class name. */ - private final Map<Class<?>, ClassDescriptor> mergedClassToDescriptorMap = new HashMap<>(); + private final Map<String, ClassDescriptor> mergedClassToDescriptorMap = new HashMap<>(); /** * A collection of the descriptors that were sent to the remote node. @@ -86,7 +86,7 @@ public class PerSessionSerializationService { this.serializationService = serializationService; this.descriptors = new CompositeDescriptorRegistry( new MapBackedIdIndexedDescriptors(mergedIdToDescriptorMap), - new MapBackedClassIndexedDescriptors(mergedClassToDescriptorMap), + new ClassNameMapBackedClassIndexedDescriptors(mergedClassToDescriptorMap), serializationService.getLocalDescriptorRegistry() ); } @@ -134,14 +134,10 @@ public class PerSessionSerializationService { * @param descriptorIds Class descriptors. * @return List of class descriptor network messages. */ - @Nullable public static List<ClassDescriptorMessage> createClassDescriptorsMessages(IntSet descriptorIds, ClassDescriptorRegistry registry) { - List<ClassDescriptorMessage> messages = descriptorIds.intStream() - .mapToObj(registry::getDescriptor) - .filter(descriptor -> { - int descriptorId = descriptor.descriptorId(); - return !shouldBeBuiltIn(descriptorId); - }) + return descriptorIds.intStream() + .mapToObj(registry::getRequiredDescriptor) + .filter(descriptor -> !shouldBeBuiltIn(descriptor.descriptorId())) .map(descriptor -> { List<FieldDescriptorMessage> fields = descriptor.fields().stream() .map(d -> { @@ -168,9 +164,8 @@ public class PerSessionSerializationService { .componentTypeName(descriptor.componentTypeName()) .attributes(classDescriptorAttributeFlags(descriptor)) .build(); - }).collect(toList()); - - return messages; + }) + .collect(toList()); } private static byte fieldFlags(FieldDescriptor fieldDescriptor) { @@ -240,11 +235,10 @@ public class PerSessionSerializationService { if (knownMergedDescriptor(classMessage.descriptorId())) { it.remove(); } else if (dependenciesAreMerged(classMessage)) { - Class<?> localClass = classForName(classMessage.className()); - ClassDescriptor mergedDescriptor = messageToMergedClassDescriptor(classMessage, localClass); + ClassDescriptor mergedDescriptor = messageToMergedClassDescriptor(classMessage); mergedIdToDescriptorMap.put(classMessage.descriptorId(), mergedDescriptor); - mergedClassToDescriptorMap.put(localClass, mergedDescriptor); + mergedClassToDescriptorMap.put(classMessage.className(), mergedDescriptor); it.remove(); @@ -271,34 +265,27 @@ public class PerSessionSerializationService { * Converts {@link ClassDescriptorMessage} to a {@link ClassDescriptor} and merges it with a local {@link ClassDescriptor} of the * same class. * - * @param clsMsg ClassDescriptorMessage. - * @param localClass the local class + * @param classMessage ClassDescriptorMessage. * @return Merged class descriptor. */ - private ClassDescriptor messageToMergedClassDescriptor(ClassDescriptorMessage clsMsg, Class<?> localClass) { - ClassDescriptor localDescriptor = serializationService.getOrCreateLocalDescriptor(localClass); - return buildRemoteDescriptor(clsMsg, localClass, localDescriptor); + private ClassDescriptor messageToMergedClassDescriptor(ClassDescriptorMessage classMessage) { + @Nullable Class<?> localClass = maybeClassForName(classMessage.className()); + + if (localClass != null) { + return buildRemoteDescriptor(classMessage, localClass); + } else { + return buildRemoteDescriptor(classMessage); + } } - private ClassDescriptor buildRemoteDescriptor( - ClassDescriptorMessage classMessage, - Class<?> localClass, - ClassDescriptor localDescriptor - ) { + private ClassDescriptor buildRemoteDescriptor(ClassDescriptorMessage classMessage, Class<?> localClass) { + ClassDescriptor localDescriptor = serializationService.getOrCreateLocalDescriptor(localClass); + List<FieldDescriptor> remoteFields = classMessage.fields().stream() - .map(fieldMsg -> fieldDescriptorFromMessage(fieldMsg, localClass)) + .map(fieldMessage -> fieldDescriptorFromMessage(fieldMessage, localClass)) .collect(toList()); - SerializationType serializationType = SerializationType.getByValue(classMessage.serializationType()); - - var serialization = new Serialization( - serializationType, - bitValue(classMessage.serializationFlags(), ClassDescriptorMessage.HAS_WRITE_OBJECT_MASK), - bitValue(classMessage.serializationFlags(), ClassDescriptorMessage.HAS_READ_OBJECT_MASK), - bitValue(classMessage.serializationFlags(), ClassDescriptorMessage.HAS_READ_OBJECT_NO_DATA_MASK), - bitValue(classMessage.serializationFlags(), ClassDescriptorMessage.HAS_WRITE_REPLACE_MASK), - bitValue(classMessage.serializationFlags(), ClassDescriptorMessage.HAS_READ_RESOLVE_MASK) - ); + Serialization serialization = buildSerialization(classMessage); return ClassDescriptor.forRemote( localClass, @@ -315,6 +302,40 @@ public class PerSessionSerializationService { ); } + private ClassDescriptor buildRemoteDescriptor(ClassDescriptorMessage classMessage) { + List<FieldDescriptor> remoteFields = classMessage.fields().stream() + .map(fieldMessage -> fieldDescriptorFromMessage(fieldMessage, classMessage.className())) + .collect(toList()); + + Serialization serialization = buildSerialization(classMessage); + + return ClassDescriptor.forRemote( + classMessage.className(), + classMessage.descriptorId(), + remoteSuperClassDescriptor(classMessage), + remoteComponentTypeDescriptor(classMessage), + bitValue(classMessage.attributes(), ClassDescriptorMessage.IS_PRIMITIVE_MASK), + bitValue(classMessage.attributes(), ClassDescriptorMessage.IS_ARRAY_MASK), + bitValue(classMessage.attributes(), ClassDescriptorMessage.IS_RUNTIME_ENUM_MASK), + bitValue(classMessage.attributes(), ClassDescriptorMessage.IS_RUNTIME_TYPE_KNOWN_UPFRONT_MASK), + remoteFields, + serialization + ); + } + + private Serialization buildSerialization(ClassDescriptorMessage classMessage) { + SerializationType serializationType = SerializationType.getByValue(classMessage.serializationType()); + + return new Serialization( + serializationType, + bitValue(classMessage.serializationFlags(), ClassDescriptorMessage.HAS_WRITE_OBJECT_MASK), + bitValue(classMessage.serializationFlags(), ClassDescriptorMessage.HAS_READ_OBJECT_MASK), + bitValue(classMessage.serializationFlags(), ClassDescriptorMessage.HAS_READ_OBJECT_NO_DATA_MASK), + bitValue(classMessage.serializationFlags(), ClassDescriptorMessage.HAS_WRITE_REPLACE_MASK), + bitValue(classMessage.serializationFlags(), ClassDescriptorMessage.HAS_READ_RESOLVE_MASK) + ); + } + private boolean bitValue(byte flags, int bitMask) { return (flags & bitMask) != 0; } @@ -335,24 +356,37 @@ public class PerSessionSerializationService { return remoteClassDescriptor(clsMsg.componentTypeDescriptorId(), clsMsg.componentTypeName()); } - private FieldDescriptor fieldDescriptorFromMessage(FieldDescriptorMessage fieldMsg, Class<?> declaringClass) { - int typeDescriptorId = fieldMsg.typeDescriptorId(); + private FieldDescriptor fieldDescriptorFromMessage(FieldDescriptorMessage fieldMessage, Class<?> declaringClass) { + int typeDescriptorId = fieldMessage.typeDescriptorId(); return FieldDescriptor.remote( - fieldMsg.name(), - fieldType(typeDescriptorId, fieldMsg.className()), + fieldMessage.name(), + fieldType(typeDescriptorId, fieldMessage.className()), typeDescriptorId, - bitValue(fieldMsg.flags(), FieldDescriptorMessage.UNSHARED_MASK), - bitValue(fieldMsg.flags(), FieldDescriptorMessage.IS_PRIMITIVE), - bitValue(fieldMsg.flags(), FieldDescriptorMessage.IS_RUNTIME_TYPE_KNOWN_UPFRONT), + bitValue(fieldMessage.flags(), FieldDescriptorMessage.UNSHARED_MASK), + bitValue(fieldMessage.flags(), FieldDescriptorMessage.IS_PRIMITIVE), + bitValue(fieldMessage.flags(), FieldDescriptorMessage.IS_RUNTIME_TYPE_KNOWN_UPFRONT), declaringClass ); } + private FieldDescriptor fieldDescriptorFromMessage(FieldDescriptorMessage fieldMessage, String declaringClassName) { + int typeDescriptorId = fieldMessage.typeDescriptorId(); + return FieldDescriptor.remote( + fieldMessage.name(), + fieldType(typeDescriptorId, fieldMessage.className()), + typeDescriptorId, + bitValue(fieldMessage.flags(), FieldDescriptorMessage.UNSHARED_MASK), + bitValue(fieldMessage.flags(), FieldDescriptorMessage.IS_PRIMITIVE), + bitValue(fieldMessage.flags(), FieldDescriptorMessage.IS_RUNTIME_TYPE_KNOWN_UPFRONT), + declaringClassName + ); + } + private ClassDescriptor remoteClassDescriptor(int descriptorId, String typeName) { if (shouldBeBuiltIn(descriptorId)) { return serializationService.getLocalDescriptor(descriptorId); } else { - return mergedClassToDescriptorMap.get(classForName(typeName)); + return mergedClassToDescriptorMap.get(typeName); } } @@ -360,15 +394,26 @@ public class PerSessionSerializationService { if (shouldBeBuiltIn(descriptorId)) { return BuiltInType.findByDescriptorId(descriptorId).clazz(); } else { - return classForName(typeName); + return requiredClassForName(typeName); } } - private Class<?> classForName(String className) { + private Class<?> requiredClassForName(String className) { + Class<?> result = maybeClassForName(className); + + if (result == null) { + throw new SerializationException("Class " + className + " is not found"); + } + + return result; + } + + @Nullable + private Class<?> maybeClassForName(String className) { try { return Class.forName(className, true, classLoader); } catch (ClassNotFoundException e) { - throw new SerializationException("Class " + className + " is not found", e); + return null; } } diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SerializationException.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SerializationException.java index 5327d5ab9..a9e0188d7 100644 --- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SerializationException.java +++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SerializationException.java @@ -19,7 +19,7 @@ package org.apache.ignite.internal.network.serialization; /** Exception that occurs during (de-)serialization. */ public class SerializationException extends RuntimeException { - public SerializationException(String message, Throwable cause) { - super(message, cause); + public SerializationException(String message) { + super(message); } } diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshaller.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshaller.java index 52b4acb31..5e95de818 100644 --- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshaller.java +++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshaller.java @@ -396,12 +396,12 @@ public class DefaultUserObjectMarshaller implements UserObjectMarshaller, Schema ) throws IOException, UnmarshalException { int objectId = readObjectId(input); - Object preInstantiatedObject = preInstantiate(remoteDescriptor, input, context); - context.registerReference(objectId, preInstantiatedObject, unshared); + Object object = preInstantiate(remoteDescriptor, input, context); + context.registerReference(objectId, object, unshared); - fillObjectFrom(input, preInstantiatedObject, remoteDescriptor, context); + fillObjectFrom(input, object, remoteDescriptor, context); - return preInstantiatedObject; + return object; } private Object preInstantiate(ClassDescriptor remoteDescriptor, IgniteDataInput input, UnmarshallingContext context) @@ -493,4 +493,9 @@ public class DefaultUserObjectMarshaller implements UserObjectMarshaller, Schema public <T> void replaceSchemaMismatchHandler(Class<T> layerClass, SchemaMismatchHandler<T> handler) { schemaMismatchHandlers.registerHandler(layerClass, handler); } + + @Override + public <T> void replaceSchemaMismatchHandler(String layerClassName, SchemaMismatchHandler<T> handler) { + schemaMismatchHandlers.registerHandler(layerClassName, handler); + } } diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/SchemaMismatchEventSource.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/SchemaMismatchEventSource.java index f032c2703..0a74295d4 100644 --- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/SchemaMismatchEventSource.java +++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/SchemaMismatchEventSource.java @@ -31,4 +31,15 @@ public interface SchemaMismatchEventSource { * @param <T> layer type */ <T> void replaceSchemaMismatchHandler(Class<T> layerClass, SchemaMismatchHandler<T> handler); + + /** + * Sets the {@link SchemaMismatchHandler} for the given class if not set or replaces the existing one. + * + * <p>Note that the handlers are per declared class, not per concrete class lineage. + * + * @param layerClassName the name of the class; for schema changes concerning this class the events will be generated + * @param handler the handler that will handle the schema mismatch events + * @param <T> layer type + */ + <T> void replaceSchemaMismatchHandler(String layerClassName, SchemaMismatchHandler<T> handler); } diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/SchemaMismatchHandlers.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/SchemaMismatchHandlers.java index a0d627a15..bc5f58561 100644 --- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/SchemaMismatchHandlers.java +++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/SchemaMismatchHandlers.java @@ -26,33 +26,41 @@ import java.util.Map; * A colleciton of {@link SchemaMismatchHandler}s keyed by classes to which they relate. */ class SchemaMismatchHandlers { - private final Map<Class<?>, SchemaMismatchHandler<?>> handlers = new HashMap<>(); + private final Map<String, SchemaMismatchHandler<?>> handlers = new HashMap<>(); private final SchemaMismatchHandler<Object> defaultHandler = new DefaultSchemaMismatchHandler(); <T> void registerHandler(Class<T> layerClass, SchemaMismatchHandler<T> handler) { - handlers.put(layerClass, handler); + registerHandler(layerClass.getName(), handler); + } + + <T> void registerHandler(String layerClassName, SchemaMismatchHandler<T> handler) { + handlers.put(layerClassName, handler); } - @SuppressWarnings("unchecked") private SchemaMismatchHandler<Object> handlerFor(Class<?> clazz) { - SchemaMismatchHandler<Object> handler = (SchemaMismatchHandler<Object>) handlers.get(clazz); + return handlerFor(clazz.getName()); + } + + @SuppressWarnings("unchecked") + private SchemaMismatchHandler<Object> handlerFor(String className) { + SchemaMismatchHandler<Object> handler = (SchemaMismatchHandler<Object>) handlers.get(className); if (handler == null) { handler = defaultHandler; } return handler; } - void onFieldIgnored(Class<?> layerClass, Object instance, String fieldName, Object fieldValue) throws SchemaMismatchException { - handlerFor(layerClass).onFieldIgnored(instance, fieldName, fieldValue); + void onFieldIgnored(String layerClassName, Object instance, String fieldName, Object fieldValue) throws SchemaMismatchException { + handlerFor(layerClassName).onFieldIgnored(instance, fieldName, fieldValue); } - void onFieldMissed(Class<?> layerClass, Object instance, String fieldName) throws SchemaMismatchException { - handlerFor(layerClass).onFieldMissed(instance, fieldName); + void onFieldMissed(String layerClassName, Object instance, String fieldName) throws SchemaMismatchException { + handlerFor(layerClassName).onFieldMissed(instance, fieldName); } - void onFieldTypeChanged(Class<?> layerClass, Object instance, String fieldName, Class<?> remoteType, Object fieldValue) + void onFieldTypeChanged(String layerClassName, Object instance, String fieldName, Class<?> remoteType, Object fieldValue) throws SchemaMismatchException { - handlerFor(layerClass).onFieldTypeChanged(instance, fieldName, remoteType, fieldValue); + handlerFor(layerClassName).onFieldTypeChanged(instance, fieldName, remoteType, fieldValue); } void onExternalizableIgnored(Object instance, ObjectInput externalData) throws SchemaMismatchException { @@ -71,11 +79,11 @@ class SchemaMismatchHandlers { handlerFor(instance.getClass()).onReadResolveDisappeared(instance); } - void onReadObjectIgnored(Class<?> layerClass, Object instance, ObjectInputStream objectData) throws SchemaMismatchException { - handlerFor(layerClass).onReadObjectIgnored(instance, objectData); + void onReadObjectIgnored(String layerClassName, Object instance, ObjectInputStream objectData) throws SchemaMismatchException { + handlerFor(layerClassName).onReadObjectIgnored(instance, objectData); } - void onReadObjectMissed(Class<?> layerClass, Object instance) throws SchemaMismatchException { - handlerFor(layerClass).onReadObjectMissed(instance); + void onReadObjectMissed(String layerClassName, Object instance) throws SchemaMismatchException { + handlerFor(layerClassName).onReadObjectMissed(instance); } } diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/StructuredObjectMarshaller.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/StructuredObjectMarshaller.java index bc4db595b..e10ec2fb0 100644 --- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/StructuredObjectMarshaller.java +++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/StructuredObjectMarshaller.java @@ -27,6 +27,7 @@ import org.apache.ignite.internal.network.serialization.DescriptorRegistry; import org.apache.ignite.internal.network.serialization.FieldAccessor; import org.apache.ignite.internal.network.serialization.FieldDescriptor; import org.apache.ignite.internal.network.serialization.MergedField; +import org.apache.ignite.internal.network.serialization.MergedLayer; import org.apache.ignite.internal.network.serialization.SpecialMethodInvocationException; import org.apache.ignite.internal.network.serialization.marshal.UosObjectInputStream.UosGetField; import org.apache.ignite.internal.network.serialization.marshal.UosObjectOutputStream.UosPutField; @@ -211,10 +212,25 @@ class StructuredObjectMarshaller implements DefaultFieldsReaderWriter { void fillStructuredObjectFrom(IgniteDataInput input, Object object, ClassDescriptor remoteDescriptor, UnmarshallingContext context) throws IOException, UnmarshalException { - List<ClassDescriptor> remoteLineage = remoteDescriptor.lineage(); + List<MergedLayer> lineage = remoteDescriptor.mergedLineage(); - for (ClassDescriptor remoteLayer : remoteLineage) { - fillStructuredObjectLayerFrom(input, remoteLayer, object, context); + for (MergedLayer mergedLayer : lineage) { + if (mergedLayer.hasRemote()) { + fillStructuredObjectLayerFrom(input, mergedLayer.remote(), object, context); + } else if (mergedLayer.hasLocal()) { + fireEventsForLocalOnlyLayer(object, mergedLayer.local()); + } + } + } + + private void fireEventsForLocalOnlyLayer(Object object, ClassDescriptor localLayer) throws SchemaMismatchException { + fireOnFieldMissedOnLayerFields(object, localLayer); + schemaMismatchHandlers.onReadObjectMissed(localLayer.className(), object); + } + + private void fireOnFieldMissedOnLayerFields(Object object, ClassDescriptor layer) throws SchemaMismatchException { + for (FieldDescriptor localField : layer.fields()) { + schemaMismatchHandlers.onFieldMissed(layer.className(), object, localField.name()); } } @@ -224,17 +240,19 @@ class StructuredObjectMarshaller implements DefaultFieldsReaderWriter { Object object, UnmarshallingContext context ) throws IOException, UnmarshalException { - ClassDescriptor localLayer = remoteLayer.local(); + boolean hasReadObjectLocally = remoteLayer.hasLocal() && remoteLayer.local().hasReadObject(); - if (remoteLayer.hasWriteObject() && localLayer.hasReadObject()) { - fillObjectWithReadObjectFrom(input, object, remoteLayer, context); - } else if (remoteLayer.hasWriteObject() && !localLayer.hasReadObject()) { - fireReadObjectIgnored(remoteLayer, object, input, context); + if (remoteLayer.hasWriteObject()) { + if (hasReadObjectLocally) { + fillObjectWithReadObjectFrom(input, object, remoteLayer, context); + } else { + fireReadObjectIgnored(remoteLayer, object, input, context); + } } else { defaultFillFieldsFrom(input, object, remoteLayer, context); - if (localLayer.hasReadObject()) { - schemaMismatchHandlers.onReadObjectMissed(remoteLayer.localClass(), object); + if (hasReadObjectLocally) { + schemaMismatchHandlers.onReadObjectMissed(remoteLayer.className(), object); } } } @@ -287,21 +305,21 @@ class StructuredObjectMarshaller implements DefaultFieldsReaderWriter { IgniteDataInput externalDataInput = new IgniteUnsafeDataInput(writeObjectDataBytes); try (var oos = new UosObjectInputStream(externalDataInput, valueReader, unsharedReader, this, context)) { - schemaMismatchHandlers.onReadObjectIgnored(remoteLayer.localClass(), object, oos); + schemaMismatchHandlers.onReadObjectIgnored(remoteLayer.className(), object, oos); } } /** {@inheritDoc} */ @Override - public void defaultFillFieldsFrom(IgniteDataInput input, Object object, ClassDescriptor descriptor, UnmarshallingContext context) + public void defaultFillFieldsFrom(IgniteDataInput input, Object object, ClassDescriptor remoteLayer, UnmarshallingContext context) throws IOException, UnmarshalException { - @Nullable BitSet nullsBitSet = NullsBitsetReader.readNullsBitSet(input, descriptor); + @Nullable BitSet nullsBitSet = NullsBitsetReader.readNullsBitSet(input, remoteLayer); - for (MergedField mergedField : descriptor.mergedFields()) { + for (MergedField mergedField : remoteLayer.mergedFields()) { if (mergedField.hasRemote()) { - fillFieldWithNullSkippedCheckFrom(input, object, mergedField, descriptor, nullsBitSet, context); + fillFieldWithNullSkippedCheckFrom(input, object, mergedField, remoteLayer, nullsBitSet, context); } else { - schemaMismatchHandlers.onFieldMissed(descriptor.localClass(), object, mergedField.name()); + schemaMismatchHandlers.onFieldMissed(remoteLayer.className(), object, mergedField.name()); } } } @@ -365,13 +383,13 @@ class StructuredObjectMarshaller implements DefaultFieldsReaderWriter { private void fireFieldIgnored(ClassDescriptor layerDescriptor, Object object, MergedField mergedField, Object fieldValue) throws SchemaMismatchException { - schemaMismatchHandlers.onFieldIgnored(layerDescriptor.localClass(), object, mergedField.name(), fieldValue); + schemaMismatchHandlers.onFieldIgnored(layerDescriptor.className(), object, mergedField.name(), fieldValue); } private void fireFieldTypeChanged(ClassDescriptor layerDescriptor, Object object, MergedField mergedField, Object fieldValue) throws SchemaMismatchException { schemaMismatchHandlers.onFieldTypeChanged( - layerDescriptor.localClass(), + layerDescriptor.className(), object, mergedField.name(), mergedField.remote().localClass(), diff --git a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/ClassDescriptorMergerTest.java b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/ClassDescriptorMergerTest.java index b206fae0d..1db4fc90f 100644 --- a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/ClassDescriptorMergerTest.java +++ b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/ClassDescriptorMergerTest.java @@ -17,10 +17,15 @@ package org.apache.ignite.internal.network.serialization; +import static java.util.stream.Collectors.toUnmodifiableList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; @@ -137,6 +142,182 @@ class ClassDescriptorMergerTest { assertThat(mergedFields.get(1).hasRemote(), is(false)); } + @Test + void mergesIdenticalLineages() { + List<ClassDescriptor> local = descriptorsForClasses("A", "B"); + List<ClassDescriptor> remote = List.copyOf(local); + + List<MergedLayer> mergedLineage = ClassDescriptorMerger.mergeLineages(local, remote); + + assertThat(mergedLineage, hasSize(2)); + + assertMergedLayerIsFull(mergedLineage.get(0), "A"); + assertMergedLayerIsFull(mergedLineage.get(1), "B"); + } + + private List<ClassDescriptor> descriptorsForClasses(String... classNames) { + return Arrays.stream(classNames) + .map(this::descriptorForClass) + .collect(toUnmodifiableList()); + } + + private ClassDescriptor descriptorForClass(String className) { + ClassDescriptor descriptor = mock(ClassDescriptor.class); + + when(descriptor.className()).thenReturn(className); + + return descriptor; + } + + private void assertMergedLayerIsFull(MergedLayer mergedLayer, String expectedClassName) { + assertThat(mergedLayer.hasLocal(), is(true)); + assertThat(mergedLayer.local().className(), is(expectedClassName)); + assertThat(mergedLayer.hasRemote(), is(true)); + assertThat(mergedLayer.remote().className(), is(expectedClassName)); + } + + @Test + void mergesLocalLineageExcessInTheBeginningToLocalOnlyPrefix() { + List<ClassDescriptor> local = descriptorsForClasses("A", "B"); + List<ClassDescriptor> remote = descriptorsForClasses("B"); + + List<MergedLayer> mergedLineage = ClassDescriptorMerger.mergeLineages(local, remote); + + assertThat(mergedLineage, hasSize(2)); + + assertMergedLayerIsLocalOnly(mergedLineage.get(0), "A"); + assertMergedLayerIsFull(mergedLineage.get(1), "B"); + } + + private void assertMergedLayerIsLocalOnly(MergedLayer mergedLayer, String expectedClassName) { + assertThat(mergedLayer.hasLocal(), is(true)); + assertThat(mergedLayer.local().className(), is(expectedClassName)); + assertThat(mergedLayer.hasRemote(), is(false)); + NullPointerException ex = assertThrows(NullPointerException.class, mergedLayer::remote); + assertThat(ex.getMessage(), is("remoteLayer is null")); + } + + @Test + void mergesLocalLineageExcessInTheEndToLocalOnlyPostfix() { + List<ClassDescriptor> local = descriptorsForClasses("A", "B"); + List<ClassDescriptor> remote = descriptorsForClasses("A"); + + List<MergedLayer> mergedLineage = ClassDescriptorMerger.mergeLineages(local, remote); + + assertThat(mergedLineage, hasSize(2)); + + assertMergedLayerIsFull(mergedLineage.get(0), "A"); + assertMergedLayerIsLocalOnly(mergedLineage.get(1), "B"); + } + + @Test + void mergesRemoteLineageExcessInTheBeginningToRemoteOnlyPrefix() { + List<ClassDescriptor> local = descriptorsForClasses("B"); + List<ClassDescriptor> remote = descriptorsForClasses("A", "B"); + + List<MergedLayer> mergedLineage = ClassDescriptorMerger.mergeLineages(local, remote); + + assertThat(mergedLineage, hasSize(2)); + + assertMergedLayerIsRemoteOnly(mergedLineage.get(0), "A"); + assertMergedLayerIsFull(mergedLineage.get(1), "B"); + } + + private void assertMergedLayerIsRemoteOnly(MergedLayer mergedLayer, String expectedClassName) { + assertThat(mergedLayer.hasRemote(), is(true)); + assertThat(mergedLayer.remote().className(), is(expectedClassName)); + assertThat(mergedLayer.hasLocal(), is(false)); + NullPointerException ex = assertThrows(NullPointerException.class, mergedLayer::local); + assertThat(ex.getMessage(), is("localLayer is null")); + } + + @Test + void mergesRemoteLineageExcessInTheEndToRemoteOnlyPostfix() { + List<ClassDescriptor> local = descriptorsForClasses("A"); + List<ClassDescriptor> remote = descriptorsForClasses("A", "B"); + + List<MergedLayer> mergedLineage = ClassDescriptorMerger.mergeLineages(local, remote); + + assertThat(mergedLineage, hasSize(2)); + + assertMergedLayerIsFull(mergedLineage.get(0), "A"); + assertMergedLayerIsRemoteOnly(mergedLineage.get(1), "B"); + } + + @Test + void mergesLocalLineageExcessInTheMiddleToLocalOnlyMiddle() { + List<ClassDescriptor> local = descriptorsForClasses("A", "B", "C"); + List<ClassDescriptor> remote = descriptorsForClasses("A", "C"); + + List<MergedLayer> mergedLineage = ClassDescriptorMerger.mergeLineages(local, remote); + + assertThat(mergedLineage, hasSize(3)); + + assertMergedLayerIsFull(mergedLineage.get(0), "A"); + assertMergedLayerIsLocalOnly(mergedLineage.get(1), "B"); + assertMergedLayerIsFull(mergedLineage.get(2), "C"); + } + + @Test + void mergesRemoteLineageExcessInTheMiddleToRemoteOnlyMiddle() { + List<ClassDescriptor> local = descriptorsForClasses("A", "C"); + List<ClassDescriptor> remote = descriptorsForClasses("A", "B", "C"); + + List<MergedLayer> mergedLineage = ClassDescriptorMerger.mergeLineages(local, remote); + + assertThat(mergedLineage, hasSize(3)); + + assertMergedLayerIsFull(mergedLineage.get(0), "A"); + assertMergedLayerIsRemoteOnly(mergedLineage.get(1), "B"); + assertMergedLayerIsFull(mergedLineage.get(2), "C"); + } + + @Test + void mergesBothLineagesExcessInTheBeginningToCorrespondingPrefix() { + List<ClassDescriptor> local = descriptorsForClasses("A", "B"); + List<ClassDescriptor> remote = descriptorsForClasses("X", "Y", "B"); + + List<MergedLayer> mergedLineage = ClassDescriptorMerger.mergeLineages(local, remote); + + assertThat(mergedLineage, hasSize(4)); + + assertMergedLayerIsLocalOnly(mergedLineage.get(0), "A"); + assertMergedLayerIsRemoteOnly(mergedLineage.get(1), "X"); + assertMergedLayerIsRemoteOnly(mergedLineage.get(2), "Y"); + assertMergedLayerIsFull(mergedLineage.get(3), "B"); + } + + @Test + void mergesBothLineagesExcessInTheEndToCorrespondingPostfix() { + List<ClassDescriptor> local = descriptorsForClasses("A", "B"); + List<ClassDescriptor> remote = descriptorsForClasses("A", "X", "Y"); + + List<MergedLayer> mergedLineage = ClassDescriptorMerger.mergeLineages(local, remote); + + assertThat(mergedLineage, hasSize(4)); + + assertMergedLayerIsFull(mergedLineage.get(0), "A"); + assertMergedLayerIsLocalOnly(mergedLineage.get(1), "B"); + assertMergedLayerIsRemoteOnly(mergedLineage.get(2), "X"); + assertMergedLayerIsRemoteOnly(mergedLineage.get(3), "Y"); + } + + @Test + void mergesBothLineagesExcessInTheMiddleToCorrespondingPostfix() { + List<ClassDescriptor> local = descriptorsForClasses("A", "B", "C"); + List<ClassDescriptor> remote = descriptorsForClasses("A", "X", "Y", "C"); + + List<MergedLayer> mergedLineage = ClassDescriptorMerger.mergeLineages(local, remote); + + assertThat(mergedLineage, hasSize(5)); + + assertMergedLayerIsFull(mergedLineage.get(0), "A"); + assertMergedLayerIsLocalOnly(mergedLineage.get(1), "B"); + assertMergedLayerIsRemoteOnly(mergedLineage.get(2), "X"); + assertMergedLayerIsRemoteOnly(mergedLineage.get(3), "Y"); + assertMergedLayerIsFull(mergedLineage.get(4), "C"); + } + private static class FieldHost { @SuppressWarnings("unused") private int test1; diff --git a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/MapBackedClassIndexedDescriptorsTest.java b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/ClassNameMapBackedClassIndexedDescriptorsTest.java similarity index 85% rename from modules/network/src/test/java/org/apache/ignite/internal/network/serialization/MapBackedClassIndexedDescriptorsTest.java rename to modules/network/src/test/java/org/apache/ignite/internal/network/serialization/ClassNameMapBackedClassIndexedDescriptorsTest.java index e8244c9fc..9c04cd824 100644 --- a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/MapBackedClassIndexedDescriptorsTest.java +++ b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/ClassNameMapBackedClassIndexedDescriptorsTest.java @@ -27,27 +27,27 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.Map; import org.junit.jupiter.api.Test; -class MapBackedClassIndexedDescriptorsTest { +class ClassNameMapBackedClassIndexedDescriptorsTest { private final ClassDescriptorRegistry unrelatedRegistry = new ClassDescriptorRegistry(); @Test void retrievesKnownDescriptorByClass() { ClassDescriptor descriptor = unrelatedRegistry.getRequiredDescriptor(String.class); - var descriptors = new MapBackedClassIndexedDescriptors(Map.of(String.class, descriptor)); + var descriptors = new ClassNameMapBackedClassIndexedDescriptors(Map.of(String.class.getName(), descriptor)); assertThat(descriptors.getDescriptor(String.class), is(descriptor)); } @Test void doesNotFindAnythingByClassWhenMapDoesNotContainTheClassDescriptor() { - var descriptors = new MapBackedClassIndexedDescriptors(emptyMap()); + var descriptors = new ClassNameMapBackedClassIndexedDescriptors(emptyMap()); assertThat(descriptors.getDescriptor(String.class), is(nullValue())); } @Test void throwsWhenQueriedAboutUnknownDescriptorByClass() { - var descriptors = new MapBackedClassIndexedDescriptors(emptyMap()); + var descriptors = new ClassNameMapBackedClassIndexedDescriptors(emptyMap()); Throwable thrownEx = assertThrows(IllegalStateException.class, () -> descriptors.getRequiredDescriptor(String.class)); assertThat(thrownEx.getMessage(), startsWith("Did not find a descriptor by class")); diff --git a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithSchemaChangeTest.java b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithSchemaChangeTest.java index 5cdb58e91..5fcd157db 100644 --- a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithSchemaChangeTest.java +++ b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithSchemaChangeTest.java @@ -28,6 +28,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -45,15 +46,19 @@ import java.io.Serializable; import java.lang.reflect.Constructor; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import net.bytebuddy.ByteBuddy; import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.implementation.StubMethod; import org.apache.ignite.internal.network.serialization.ClassDescriptor; import org.apache.ignite.internal.network.serialization.ClassDescriptorFactory; import org.apache.ignite.internal.network.serialization.ClassDescriptorRegistry; +import org.apache.ignite.internal.network.serialization.ClassNameMapBackedClassIndexedDescriptors; import org.apache.ignite.internal.network.serialization.CompositeDescriptorRegistry; import org.apache.ignite.internal.network.serialization.FieldDescriptor; -import org.apache.ignite.internal.network.serialization.MapBackedClassIndexedDescriptors; import org.apache.ignite.internal.network.serialization.MapBackedIdIndexedDescriptors; import org.apache.ignite.internal.testframework.IgniteTestUtils; import org.jetbrains.annotations.Nullable; @@ -72,6 +77,8 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) public class DefaultUserObjectMarshallerWithSchemaChangeTest { private static final byte[] INT_42_BYTES_IN_LITTLE_ENDIAN = {42, 0, 0, 0}; + private static final String NON_LEAF_CLASS_NAME = "test.NonLeaf"; + private static final String LEAF_CLASS_NAME = "test.Leaf"; private final ClassDescriptorRegistry localRegistry = new ClassDescriptorRegistry(); private final ClassDescriptorFactory localFactory = new ClassDescriptorFactory(localRegistry); @@ -103,9 +110,9 @@ public class DefaultUserObjectMarshallerWithSchemaChangeTest { verify(schemaMismatchHandler).onFieldIgnored(unmarshalled, "addedRemotely", extraField.value); } - private Class<?> addFieldTo(Class<?> localClass, String fieldName, Class<?> fieldType) { + private Class<?> addFieldTo(Class<?> baseClass, String fieldName, Class<?> fieldType) { return new ByteBuddy() - .redefine(localClass) + .redefine(baseClass) .defineField(fieldName, fieldType, Visibility.PRIVATE) .make() .load(getClass().getClassLoader(), CHILD_FIRST) @@ -205,6 +212,16 @@ public class DefaultUserObjectMarshallerWithSchemaChangeTest { return unmarshalNotNullLocally(marshalled, localClass, remoteClass); } + private Object marshalRemotelyAndUnmarshalLocally( + Object remoteInstance, + Class<?> localClass, + Class<?> remoteClass, + Function<ClassDescriptor, ClassDescriptor> reconstructSuperDescriptor + ) throws MarshalException, UnmarshalException { + MarshalledObject marshalled = remoteMarshaller.marshal(remoteInstance); + return unmarshalNotNullLocally(marshalled, localClass, remoteClass, reconstructSuperDescriptor); + } + private <T> T unmarshalNotNullLocally(MarshalledObject marshalled, Class<?> localClass, Class<?> remoteClass) throws UnmarshalException { T unmarshalled = unmarshalLocally(marshalled, localClass, remoteClass); @@ -212,9 +229,30 @@ public class DefaultUserObjectMarshallerWithSchemaChangeTest { return unmarshalled; } + private <T> T unmarshalNotNullLocally( + MarshalledObject marshalled, + Class<?> localClass, + Class<?> remoteClass, + Function<ClassDescriptor, ClassDescriptor> reconstructSuperDescriptor + ) throws UnmarshalException { + T unmarshalled = unmarshalLocally(marshalled, localClass, remoteClass, reconstructSuperDescriptor); + assertThat(unmarshalled, is(notNullValue())); + return unmarshalled; + } + @Nullable private <T> T unmarshalLocally(MarshalledObject marshalled, Class<?> localClass, Class<?> remoteClass) throws UnmarshalException { + return unmarshalLocally(marshalled, localClass, remoteClass, Function.identity()); + } + + @Nullable + private <T> T unmarshalLocally( + MarshalledObject marshalled, + Class<?> localClass, + Class<?> remoteClass, + Function<ClassDescriptor, ClassDescriptor> reconstructSuperDescriptor + ) throws UnmarshalException { localFactory.create(localClass); ClassDescriptor localDescriptor = localRegistry.getRequiredDescriptor(localClass); @@ -223,7 +261,7 @@ public class DefaultUserObjectMarshallerWithSchemaChangeTest { ClassDescriptor reconstructedRemoteDescriptor = ClassDescriptor.forRemote( localDescriptor.localClass(), remoteDescriptor.descriptorId(), - remoteDescriptor.superClassDescriptor(), + reconstructSuperDescriptor.apply(remoteDescriptor.superClassDescriptor()), remoteDescriptor.componentTypeDescriptor(), remoteDescriptor.isPrimitive(), remoteDescriptor.isArray(), @@ -238,7 +276,9 @@ public class DefaultUserObjectMarshallerWithSchemaChangeTest { new MapBackedIdIndexedDescriptors( Int2ObjectMaps.singleton(reconstructedRemoteDescriptor.descriptorId(), reconstructedRemoteDescriptor) ), - new MapBackedClassIndexedDescriptors(Map.of(reconstructedRemoteDescriptor.localClass(), reconstructedRemoteDescriptor)), + new ClassNameMapBackedClassIndexedDescriptors( + Map.of(reconstructedRemoteDescriptor.localClass().getName(), reconstructedRemoteDescriptor) + ), localRegistry ); @@ -278,6 +318,196 @@ public class DefaultUserObjectMarshallerWithSchemaChangeTest { } } + @Test + void whenClassIsMergedIntoItsSubclassLocallyThenItsFieldsShouldNotBeFilledOnUnmarshalling() throws Exception { + Object unmarshalled = marshalRemotelyAndUnmarshalWithSuperclassDisappearingLocally(); + + assertThat(IgniteTestUtils.getFieldValue(unmarshalled, unmarshalled.getClass(), "value1"), is(0)); + } + + @SuppressWarnings("unchecked") + @Test + void whenClassIsMergedIntoItsSubclassLocallyThenItsFieldsShouldTriggerFieldMissedAndIgnoredEventsOnUnmarshalling() throws Exception { + SchemaMismatchHandler<Object> nonLeafLayerHandler = mock(SchemaMismatchHandler.class); + SchemaMismatchHandler<Object> leafLayerHandler = mock(SchemaMismatchHandler.class); + + localMarshaller.replaceSchemaMismatchHandler(LEAF_CLASS_NAME, leafLayerHandler); + localMarshaller.replaceSchemaMismatchHandler(NON_LEAF_CLASS_NAME, nonLeafLayerHandler); + + Object unmarshalled = marshalRemotelyAndUnmarshalWithSuperclassDisappearingLocally(); + + verify(leafLayerHandler).onFieldMissed(unmarshalled, "value1"); + verify(nonLeafLayerHandler).onFieldIgnored(unmarshalled, "value1", 1); + } + + @SuppressWarnings("unchecked") + @Test + void whenClassWithWriteObjectMethodIsMergedIntoItsSubclassLocallyThenReadObjectIgnoredEventShouldBeTriggeredOnUnmarshalling() + throws Exception { + SchemaMismatchHandler<Object> nonLeafLayerHandler = mock(SchemaMismatchHandler.class); + + localMarshaller.replaceSchemaMismatchHandler(NON_LEAF_CLASS_NAME, nonLeafLayerHandler); + + Object unmarshalled = marshalRemotelyAndUnmarshalWithSuperclassDisappearingLocally(this::withEmptyWriteObjectMethod); + + verify(nonLeafLayerHandler).onReadObjectIgnored(eq(unmarshalled), any()); + } + + private DynamicType.Builder<Object> withEmptyWriteObjectMethod(DynamicType.Builder<Object> builder) { + return builder + .implement(Serializable.class) + .defineMethod("writeObject", void.class, Visibility.PRIVATE) + .withParameters(ObjectOutputStream.class) + .intercept(StubMethod.INSTANCE); + } + + private Object marshalRemotelyAndUnmarshalWithSuperclassDisappearingLocally() + throws MarshalException, ReflectiveOperationException, UnmarshalException { + return marshalRemotelyAndUnmarshalWithSuperclassDisappearingLocally(UnaryOperator.identity()); + } + + private Object marshalRemotelyAndUnmarshalWithSuperclassDisappearingLocally( + UnaryOperator<DynamicType.Builder<Object>> remoteNonLeafClassCustomizer + ) throws ReflectiveOperationException, MarshalException, UnmarshalException { + + DynamicType.Builder<Object> remoteNonLeafClassBuilder = new ByteBuddy() + .subclass(Object.class) + .name(NON_LEAF_CLASS_NAME) + .defineField("value1", int.class, Visibility.PRIVATE); + + Class<?> remoteNonLeafClass = remoteNonLeafClassCustomizer.apply(remoteNonLeafClassBuilder) + .make() + .load(getClass().getClassLoader(), CHILD_FIRST) + .getLoaded(); + Class<?> remoteLeafClass = new ByteBuddy() + .subclass(remoteNonLeafClass) + .name(LEAF_CLASS_NAME) + .defineField("value2", int.class, Visibility.PRIVATE) + .make() + .load(remoteNonLeafClass.getClassLoader()) + .getLoaded(); + + Class<?> localLeafClass = new ByteBuddy() + .subclass(Object.class) + .name(LEAF_CLASS_NAME) + .defineField("value1", int.class, Visibility.PRIVATE) + .defineField("value2", int.class, Visibility.PRIVATE) + .make() + .load(getClass().getClassLoader(), CHILD_FIRST) + .getLoaded(); + + Object remoteInstance = instantiate(remoteLeafClass); + IgniteTestUtils.setFieldValue(remoteInstance, remoteInstance.getClass().getSuperclass(), "value1", 1); + IgniteTestUtils.setFieldValue(remoteInstance, remoteLeafClass, "value2", 2); + + return marshalRemotelyAndUnmarshalLocally( + remoteInstance, + localLeafClass, + remoteLeafClass, + this::toRemoteDescriptorWithoutLocalClass + ); + } + + private ClassDescriptor toRemoteDescriptorWithoutLocalClass(ClassDescriptor superDescriptor) { + return ClassDescriptor.forRemote( + superDescriptor.className(), + superDescriptor.descriptorId(), + superDescriptor.superClassDescriptor(), + superDescriptor.componentTypeDescriptor(), + superDescriptor.isPrimitive(), + superDescriptor.isArray(), + superDescriptor.isRuntimeEnum(), + superDescriptor.isRuntimeTypeKnownUpfront(), + superDescriptor.fields(), + superDescriptor.serialization() + ); + } + + @Test + void whenSuperclassIsSplitFromClassLocallyThenSuperclassFieldsShouldNotBeFilledOnUnmarshalling() throws Exception { + Object unmarshalled = marshalRemotelyAndUnmarshalWithSuperclassAppearingLocally(); + + assertThat(IgniteTestUtils.getFieldValue(unmarshalled, unmarshalled.getClass().getSuperclass(), "value1"), is(0)); + } + + @SuppressWarnings("unchecked") + @Test + void whenSuperclassIsSplitFromClassLocallyThenSuperclassFieldsShouldTriggerFieldMissedAndIgnoredEventsOnUnmarshalling() + throws Exception { + SchemaMismatchHandler<Object> nonLeafLayerHandler = mock(SchemaMismatchHandler.class); + SchemaMismatchHandler<Object> leafLayerHandler = mock(SchemaMismatchHandler.class); + + localMarshaller.replaceSchemaMismatchHandler(LEAF_CLASS_NAME, leafLayerHandler); + localMarshaller.replaceSchemaMismatchHandler(NON_LEAF_CLASS_NAME, nonLeafLayerHandler); + + Object unmarshalled = marshalRemotelyAndUnmarshalWithSuperclassAppearingLocally(); + + verify(leafLayerHandler).onFieldIgnored(unmarshalled, "value1", 1); + verify(nonLeafLayerHandler).onFieldMissed(unmarshalled, "value1"); + } + + @SuppressWarnings("unchecked") + @Test + void whenSuperclassWithReadObjectMethodIsSplitFromClassLocallyThenReadObjectMissedEventShouldBeTriggeredOnUnmarshalling() + throws Exception { + SchemaMismatchHandler<Object> nonLeafLayerHandler = mock(SchemaMismatchHandler.class); + + localMarshaller.replaceSchemaMismatchHandler(NON_LEAF_CLASS_NAME, nonLeafLayerHandler); + + Object unmarshalled = marshalRemotelyAndUnmarshalWithSuperclassAppearingLocally(this::withEmptyReadObjectMethod); + + verify(nonLeafLayerHandler).onReadObjectMissed(eq(unmarshalled)); + } + + private DynamicType.Builder<Object> withEmptyReadObjectMethod(DynamicType.Builder<Object> builder) { + return builder + .implement(Serializable.class) + .defineMethod("readObject", void.class, Visibility.PRIVATE) + .withParameters(ObjectInputStream.class) + .intercept(StubMethod.INSTANCE); + } + + private Object marshalRemotelyAndUnmarshalWithSuperclassAppearingLocally() + throws ReflectiveOperationException, MarshalException, UnmarshalException { + return marshalRemotelyAndUnmarshalWithSuperclassAppearingLocally(UnaryOperator.identity()); + } + + private Object marshalRemotelyAndUnmarshalWithSuperclassAppearingLocally( + UnaryOperator<DynamicType.Builder<Object>> localNonLeafClassCustomizer + ) throws ReflectiveOperationException, MarshalException, UnmarshalException { + + Class<?> remoteLeafClass = new ByteBuddy() + .subclass(Object.class) + .name(LEAF_CLASS_NAME) + .defineField("value1", int.class, Visibility.PRIVATE) + .defineField("value2", int.class, Visibility.PRIVATE) + .make() + .load(getClass().getClassLoader(), CHILD_FIRST) + .getLoaded(); + + DynamicType.Builder<Object> localNonLeafClassBuilder = new ByteBuddy() + .subclass(Object.class) + .name(NON_LEAF_CLASS_NAME) + .defineField("value1", int.class, Visibility.PRIVATE); + Class<?> localNonLeafClass = localNonLeafClassCustomizer.apply(localNonLeafClassBuilder) + .make() + .load(getClass().getClassLoader(), CHILD_FIRST) + .getLoaded(); + Class<?> localLeafClass = new ByteBuddy() + .subclass(localNonLeafClass) + .name(LEAF_CLASS_NAME) + .defineField("value2", int.class, Visibility.PRIVATE) + .make() + .load(localNonLeafClass.getClassLoader()) + .getLoaded(); + + Object remoteInstance = instantiate(remoteLeafClass); + IgniteTestUtils.setFieldValue(remoteInstance, remoteInstance.getClass(), "value1", 1); + IgniteTestUtils.setFieldValue(remoteInstance, remoteInstance.getClass(), "value2", 2); + + return marshalRemotelyAndUnmarshalLocally(remoteInstance, localLeafClass, remoteLeafClass); + } + @ParameterizedTest @MethodSource("extraFieldsForGetField") void getFieldReturnsDefaultValueWhenRemoteClassHasExtraField(ExtraFieldForGetField extraField) throws Exception { @@ -589,7 +819,7 @@ public class DefaultUserObjectMarshallerWithSchemaChangeTest { private void writeObject(ObjectOutputStream stream) throws IOException { stream.putFields(); - stream.writeFields();; + stream.writeFields(); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {