From 732544616f4c5fec2f0d508b428b56225a0a675a Mon Sep 17 00:00:00 2001 From: zstan Date: Wed, 21 Oct 2026 10:02:33 +0300 Subject: [PATCH 1/3] IGNITE-29172 Thin client caches incomplete binary type metadata if a channel fails while a binary object is being built --- .../ignite/internal/binary/BinaryContext.java | 25 +- .../internal/binary/BinaryMetadata.java | 31 ++ .../binary/BinaryMetadataHandler.java | 2 +- .../builder/BinaryObjectBuilderImpl.java | 45 +-- .../CacheObjectBinaryProcessorImpl.java | 2 + ...niteClientReconnectBinaryContextTest.java} | 2 +- ...lientReconnectCustomBinaryContextTest.java | 288 ++++++++++++++++++ .../BinaryMetadataChannelFailureTest.java | 168 ++++++++++ .../IgniteClientReconnectTestSuite.java | 6 +- .../apache/ignite/client/ClientTestSuite.java | 2 + .../internal/client/thin/TcpIgniteClient.java | 8 + 11 files changed, 555 insertions(+), 24 deletions(-) rename modules/core/src/test/java/org/apache/ignite/internal/{IgniteClientReconnectBinaryContexTest.java => IgniteClientReconnectBinaryContextTest.java} (97%) create mode 100644 modules/core/src/test/java/org/apache/ignite/internal/IgniteClientReconnectCustomBinaryContextTest.java create mode 100644 modules/core/src/test/java/org/apache/ignite/internal/client/thin/BinaryMetadataChannelFailureTest.java diff --git a/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java b/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java index d5e08498535bd..88348f652f48d 100644 --- a/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java +++ b/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java @@ -1361,14 +1361,25 @@ public boolean isCompactFooter() { return compactFooter; } - /** */ + /** + * Update binary meta if needed. + * + * @param writer Binary writer. + * @param meta Binary type. + * @param typeId Type ID. + * @param typeName Type name. + * @param affFieldName Affinity name. + * @param fieldsMeta Fields meta holder. + * @param partialFields It will be {@code true} if found already known object fields. + */ public void updateMetaIfNeeded( BinaryWriterEx writer, BinaryType meta, int typeId, String typeName, String affFieldName, - Map fieldsMeta + Map fieldsMeta, + boolean partialFields ) { BinarySchemaRegistry schemaReg = schemaRegistry(typeId); @@ -1390,7 +1401,7 @@ public void updateMetaIfNeeded( registerUserClassName(typeId, typeName, writer.failIfUnregistered(), false, JAVA_ID); updateMetadata(typeId, new BinaryMetadata(typeId, typeName, fieldsMeta, affFieldName, - Collections.singleton(curSchema), false, null), writer.failIfUnregistered()); + Collections.singleton(curSchema), false, null, partialFields), writer.failIfUnregistered()); schemaReg.addSchema(curSchema.schemaId(), curSchema); } @@ -1444,6 +1455,14 @@ BinarySchemaRegistry schemaRegistry(int typeId) { return reg; } + /** Restores preconfigured user types after reconnect. */ + public void onReconnect() { + for (BinaryClassDescriptor desc : predefinedTypes.values()) { + if (desc.userType()) + descByCls.put(desc.describedClass(), desc); + } + } + /** * Unregister all binary schemas. */ diff --git a/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryMetadata.java b/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryMetadata.java index 9fb8a918fa6fe..def287dca2be0 100644 --- a/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryMetadata.java +++ b/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryMetadata.java @@ -76,6 +76,10 @@ public class BinaryMetadata implements Externalizable { /** Enum ordinal to name mapping. */ private Map ordinalToName; + /** Flag {@code true} if for optimization needs, already known object fields are not pushed into + * {@link #fields} structure. Not used during serialization. */ + private boolean partialFields; + /** * For {@link Externalizable}. */ @@ -98,6 +102,27 @@ public BinaryMetadata(int typeId, String typeName, @Nullable Map fields, + @Nullable String affKeyFieldName, @Nullable Collection schemas, boolean isEnum, + @Nullable Map enumMap, boolean partialFields) { + this(typeId, typeName, fields, affKeyFieldName, schemas, isEnum, enumMap); + + this.partialFields = partialFields; + } + /** * Constructor. * @@ -211,6 +236,12 @@ public boolean isEnum() { return isEnum; } + /** @return {@code true} if for optimization needs, already known object fields are not pushed + * into {@link #fields} structure. */ + public boolean isPartialFields() { + return partialFields; + } + /** * Wrap metadata into binary type. * diff --git a/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryMetadataHandler.java b/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryMetadataHandler.java index 557a4f314c330..d1321dd8fb0b1 100644 --- a/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryMetadataHandler.java +++ b/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryMetadataHandler.java @@ -37,7 +37,7 @@ public interface BinaryMetadataHandler { public void addMeta(int typeId, BinaryType meta, boolean failIfUnregistered) throws BinaryObjectException; /** - * Adds meta data locally on current node without sending any messages. + * Adds metadata locally on current node without sending any messages. * * @param typeId Type ID. * @param meta Metadata. diff --git a/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/builder/BinaryObjectBuilderImpl.java b/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/builder/BinaryObjectBuilderImpl.java index c0a66261d5b65..307a9d2951a73 100644 --- a/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/builder/BinaryObjectBuilderImpl.java +++ b/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/builder/BinaryObjectBuilderImpl.java @@ -42,6 +42,7 @@ import org.apache.ignite.internal.marshaller.ClassLoaderUtils; import org.apache.ignite.internal.util.CommonUtils; import org.apache.ignite.internal.util.typedef.F; +import org.apache.ignite.internal.util.typedef.T1; import org.apache.ignite.lang.IgniteBiTuple; import org.apache.ignite.thread.IgniteThread; import org.jetbrains.annotations.Nullable; @@ -208,6 +209,10 @@ void serializeTo(BinaryWriterExImpl writer, BinaryBuilderSerializer serializer) Map fieldsMeta = null; + // For optimization needs already known object fields are not pushed into structure. + // This flag signals about such optimizations. + T1 partialFields = new T1<>(false); + if (reader != null && BinaryImplUtils.hasSchema(flags)) { Map assignedFldsById; @@ -223,7 +228,7 @@ void serializeTo(BinaryWriterExImpl writer, BinaryBuilderSerializer serializer) assignedFldsById.put(fieldId, val); if (val != REMOVED_FIELD_MARKER) - fieldsMeta = checkMetadata(meta, fieldsMeta, val, name, fieldId); + fieldsMeta = checkMetadata(meta, fieldsMeta, val, name, fieldId, partialFields); } remainsFlds = assignedFldsById.keySet(); @@ -315,9 +320,10 @@ else if (readCache == null) { serializer.writeValue(writer, val); - if (reader == null) + if (reader == null) { // Metadata has already been checked. - fieldsMeta = checkMetadata(meta, fieldsMeta, val, name, fieldId); + fieldsMeta = checkMetadata(meta, fieldsMeta, val, name, fieldId, partialFields); + } } } @@ -338,7 +344,7 @@ else if (readCache == null) { writer.postWrite(true, registeredType); - ctx.updateMetaIfNeeded(writer, meta, typeId, typeName, affFieldName, fieldsMeta); + ctx.updateMetaIfNeeded(writer, meta, typeId, typeName, affFieldName, fieldsMeta, partialFields.get()); // Update hash code after schema is written. writer.postWriteHashCode(registeredType ? null : clsNameToWrite); @@ -356,9 +362,10 @@ else if (readCache == null) { * @param newVal Field value being serialized. * @param name Field name. * @param fieldId Field ID. + * @param partialFields It will be {@code true} if found already known object fields. */ private Map checkMetadata(BinaryType meta, Map fieldsMeta, - Object newVal, String name, int fieldId) { + Object newVal, String name, int fieldId, T1 partialFields) { String oldFldTypeName = meta == null ? null : meta.fieldTypeName(name); boolean nullFieldVal = false; @@ -402,18 +409,22 @@ else if (BinaryUtils.isBinaryArray(newVal)) fieldsMeta.put(name, new BinaryFieldMetadata(newFldTypeId, fieldId)); } - else if (!nullFieldVal) { - String newFldTypeName = BinaryUtils.fieldTypeName(newFldTypeId); - - if (!Objects.equals(newFldTypeName, oldFldTypeName) && - !oldFldTypeName.equals(BinaryUtils.fieldTypeName(GridBinaryMarshaller.OBJ))) { - throw new BinaryObjectException( - "Wrong value has been set [" + - "typeName=" + (typeName == null ? meta.typeName() : typeName) + - ", fieldName=" + name + - ", fieldType=" + oldFldTypeName + - ", assignedValueType=" + newFldTypeName + ']' - ); + else { + partialFields.set(true); + + if (!nullFieldVal) { + String newFldTypeName = BinaryUtils.fieldTypeName(newFldTypeId); + + if (!Objects.equals(newFldTypeName, oldFldTypeName) && + !oldFldTypeName.equals(BinaryUtils.fieldTypeName(GridBinaryMarshaller.OBJ))) { + throw new BinaryObjectException( + "Wrong value has been set [" + + "typeName=" + (typeName == null ? meta.typeName() : typeName) + + ", fieldName=" + name + + ", fieldType=" + oldFldTypeName + + ", assignedValueType=" + newFldTypeName + ']' + ); + } } } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/CacheObjectBinaryProcessorImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/CacheObjectBinaryProcessorImpl.java index cab90a3f30d13..30ddfe4e706f4 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/CacheObjectBinaryProcessorImpl.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/CacheObjectBinaryProcessorImpl.java @@ -339,6 +339,8 @@ public CacheObjectBinaryProcessorImpl(GridKernalContext ctx) { @Override public IgniteInternalFuture onReconnected(boolean clusterRestarted) throws IgniteCheckedException { reconnectFut = null; + binaryContext().onReconnect(); + return super.onReconnected(clusterRestarted); } diff --git a/modules/core/src/test/java/org/apache/ignite/internal/IgniteClientReconnectBinaryContexTest.java b/modules/core/src/test/java/org/apache/ignite/internal/IgniteClientReconnectBinaryContextTest.java similarity index 97% rename from modules/core/src/test/java/org/apache/ignite/internal/IgniteClientReconnectBinaryContexTest.java rename to modules/core/src/test/java/org/apache/ignite/internal/IgniteClientReconnectBinaryContextTest.java index ec053828e1509..9b745d2012950 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/IgniteClientReconnectBinaryContexTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/IgniteClientReconnectBinaryContextTest.java @@ -30,7 +30,7 @@ /** * */ -public class IgniteClientReconnectBinaryContexTest extends IgniteClientReconnectAbstractTest { +public class IgniteClientReconnectBinaryContextTest extends IgniteClientReconnectAbstractTest { /** {@inheritDoc} */ @Override protected int serverCount() { return 1; diff --git a/modules/core/src/test/java/org/apache/ignite/internal/IgniteClientReconnectCustomBinaryContextTest.java b/modules/core/src/test/java/org/apache/ignite/internal/IgniteClientReconnectCustomBinaryContextTest.java new file mode 100644 index 0000000000000..d29c7b1d2e390 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/IgniteClientReconnectCustomBinaryContextTest.java @@ -0,0 +1,288 @@ +/* + * 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; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.Temporal; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.ignite.Ignite; +import org.apache.ignite.IgniteCheckedException; +import org.apache.ignite.IgniteClientDisconnectedException; +import org.apache.ignite.binary.BinaryBasicIdMapper; +import org.apache.ignite.binary.BinaryBasicNameMapper; +import org.apache.ignite.binary.BinaryObjectException; +import org.apache.ignite.binary.BinaryReader; +import org.apache.ignite.binary.BinarySerializer; +import org.apache.ignite.binary.BinaryTypeConfiguration; +import org.apache.ignite.binary.BinaryWriter; +import org.apache.ignite.cache.CacheAtomicityMode; +import org.apache.ignite.cache.CacheMode; +import org.apache.ignite.cache.CacheWriteSynchronizationMode; +import org.apache.ignite.configuration.BinaryConfiguration; +import org.apache.ignite.configuration.CacheConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.spi.discovery.tcp.TcpDiscoveryIoSession; +import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi; +import org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinder; +import org.apache.ignite.spi.discovery.tcp.messages.TcpDiscoveryAbstractMessage; +import org.apache.ignite.testframework.GridTestUtils; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.Test; +import org.springframework.util.ReflectionUtils; + +import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoField.HOUR_OF_DAY; +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; +import static java.time.temporal.ChronoField.MONTH_OF_YEAR; +import static java.time.temporal.ChronoField.NANO_OF_SECOND; +import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; +import static java.time.temporal.ChronoField.YEAR; + +/** + * Test binary configuration restored after client reconnect. + */ +public class IgniteClientReconnectCustomBinaryContextTest extends GridCommonAbstractTest { + /** */ + public static final String CACHE_NAME = "Test"; + + /** */ + private final AtomicBoolean block = new AtomicBoolean(false); + + /** */ + @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { + IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName); + + cfg.setBinaryConfiguration( + new BinaryConfiguration() + .setTypeConfigurations( + Arrays.asList( + new BinaryTypeConfiguration(DateKey.class.getName()) + .setIdMapper(new BinaryBasicIdMapper(true)) + .setNameMapper(new BinaryBasicNameMapper(true)), + new BinaryTypeConfiguration("java.time.Ser") + .setSerializer(new LocalDateBinarySerializer()) + ) + ) + ); + + if (igniteInstanceName.equals("client")) { + TcpDiscoverySpi discoverySpi = (TcpDiscoverySpi)cfg.getDiscoverySpi(); + TcpDiscoveryIpFinder ipFinder = discoverySpi.getIpFinder(); + + BlockingDiscoverySpi clientDiscoverySpi = new BlockingDiscoverySpi(block); + clientDiscoverySpi.setIpFinder(ipFinder); + + cfg.setDiscoverySpi(clientDiscoverySpi); + + cfg.setFailureDetectionTimeout(30000); + cfg.setClientFailureDetectionTimeout(30000); + } + + return cfg; + } + + /** */ + @Test + public void testReconnect() throws Exception { + IgniteEx srv1 = startGrid(0); + IgniteEx client = startClientGrid("client"); + + srv1.getOrCreateCache(getCacheConfiguration()); + + DateKey storedKey = new DateKey(LocalDate.now()); + String val = "Hello"; + + // Put value to cache and ensure it is visible from client and server. + srv1.cache(CACHE_NAME).put(storedKey, val); + assertEquals(val, srv1.cache(CACHE_NAME).get(storedKey)); + assertEquals(val, client.cache(CACHE_NAME).get(storedKey)); + + // Wait for client disconnected. + block.set(true); + assertTrue(GridTestUtils.waitForCondition(() -> { + try { + client.cache(CACHE_NAME).get(storedKey); + } + catch (IgniteClientDisconnectedException ex) { + return true; + } + return false; + }, getTestTimeout())); + + assertTrue(client.context().clientDisconnected()); + block.set(false); + + // Wait for client to reconnect. + try { + client.cache(CACHE_NAME); + } + catch (IgniteClientDisconnectedException ex) { + ex.reconnectFuture().get(); + } + + // Ensure value is still there. + assertEquals(val, client.cache(CACHE_NAME).get(storedKey)); + + client.close(); + + Ignite client1 = startClientGrid("client"); + assertEquals(val, client1.cache(CACHE_NAME).get(storedKey)); + } + + /** */ + private static CacheConfiguration getCacheConfiguration() { + CacheConfiguration ccfg = new CacheConfiguration<>(); + + ccfg.setName(CACHE_NAME); + ccfg.setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_SYNC); + ccfg.setAtomicityMode(CacheAtomicityMode.ATOMIC); + ccfg.setTypes(DateKey.class, String.class); + ccfg.setIndexedTypes(DateKey.class, String.class); + ccfg.setCacheMode(CacheMode.PARTITIONED); + + return ccfg; + } + + /** */ + public static class DateKey { + /** */ + LocalDate date; + + /** */ + public DateKey(LocalDate date) { + this.date = date; + } + } + + /** */ + public static class LocalDateBinarySerializer implements BinarySerializer { + /** */ + private static final byte DURATION_TYPE = 1; + + /** */ + private static final byte LOCAL_DATE_TYPE = 3; + + /** */ + private static final byte LOCAL_DATE_TIME_TYPE = 5; + + /** */ + private final Field objectField; + + /** */ + private final Field typeField; + + /** */ + public LocalDateBinarySerializer() { + try { + Class cls = Class.forName("java.time.Ser"); + objectField = ReflectionUtils.findField(cls, "object"); + objectField.setAccessible(true); + + typeField = ReflectionUtils.findField(cls, "type"); + typeField.setAccessible(true); + } + catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** */ + @Override public void writeBinary(Object obj, BinaryWriter writer) throws BinaryObjectException { + Byte type = (Byte)ReflectionUtils.getField(typeField, obj); + writer.writeByte("type", type); + if (type == DURATION_TYPE) { + Duration duration = (Duration)ReflectionUtils.getField(objectField, obj); + writer.writeString("duration", duration != null ? duration.toString() : null); + } + else { + Temporal date = (Temporal)ReflectionUtils.getField(objectField, obj); + writer.writeInt("year", date.get(YEAR)); + writer.writeInt("month", date.get(MONTH_OF_YEAR)); + writer.writeInt("day", date.get(DAY_OF_MONTH)); + if (type == LOCAL_DATE_TIME_TYPE) { + writer.writeInt("hour", date.get(HOUR_OF_DAY)); + writer.writeInt("minute", date.get(MINUTE_OF_HOUR)); + writer.writeInt("second", date.get(SECOND_OF_MINUTE)); + writer.writeInt("nano", date.get(NANO_OF_SECOND)); + } + } + } + + /** */ + @Override public void readBinary(Object obj, BinaryReader reader) throws BinaryObjectException { + byte type = reader.readByte("type"); + + if (type == DURATION_TYPE) { + String str = reader.readString("duration"); + Duration duration = str != null ? Duration.parse(str) : null; + ReflectionUtils.setField(objectField, obj, duration); + + } + else { + int year = reader.readInt("year"); + int month = reader.readInt("month"); + int day = reader.readInt("day"); + + LocalDate date = LocalDate.of(year, month, day); + if (type == LOCAL_DATE_TYPE) { + ReflectionUtils.setField(objectField, obj, date); + } + else { + LocalTime time = LocalTime.of( + reader.readInt("hour"), + reader.readInt("minute"), + reader.readInt("second"), + reader.readInt("nano") + ); + LocalDateTime dateTime = LocalDateTime.of(date, time); + ReflectionUtils.setField(objectField, obj, dateTime); + } + } + } + } + + /** + * Discovery SPI that stops processing messages when {@code block} is set to {@code true}. + */ + private static class BlockingDiscoverySpi extends TcpDiscoverySpi { + /** */ + private final AtomicBoolean block; + + /** */ + public BlockingDiscoverySpi(AtomicBoolean block) { + this.block = block; + } + + /** */ + @Override protected void writeMessage( + TcpDiscoveryIoSession ses, + TcpDiscoveryAbstractMessage msg, + long timeout + ) throws IOException, IgniteCheckedException { + if (!block.get()) + super.writeMessage(ses, msg, timeout); + } + } +} + diff --git a/modules/core/src/test/java/org/apache/ignite/internal/client/thin/BinaryMetadataChannelFailureTest.java b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/BinaryMetadataChannelFailureTest.java new file mode 100644 index 0000000000000..e2c5130d88f7f --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/BinaryMetadataChannelFailureTest.java @@ -0,0 +1,168 @@ +/* + * 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.client.thin; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.apache.ignite.Ignition; +import org.apache.ignite.binary.BinaryBasicIdMapper; +import org.apache.ignite.binary.BinaryIdMapper; +import org.apache.ignite.binary.BinaryObject; +import org.apache.ignite.binary.BinaryObjectBuilder; +import org.apache.ignite.client.ClientCache; +import org.apache.ignite.client.IgniteClient; +import org.apache.ignite.configuration.BinaryConfiguration; +import org.apache.ignite.configuration.ClientConfiguration; +import org.apache.ignite.internal.IgniteEx; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/** + * Checks the thin client binary metadata cache when a channel fails while a binary object is being built. + */ +@RunWith(Parameterized.class) +public class BinaryMetadataChannelFailureTest extends AbstractThinClientTest { + /** Type name. */ + private static final String TYPE_NAME = "TestType"; + + /** Field names. */ + private static final List FIELDS = Arrays.asList("f0", "f1", "f2"); + + /** Field names. */ + private static final List FIELDS_NEW = Arrays.asList("f0", "f1", "f2", "f3"); + + /** Field whose ID resolution triggers the channel failure. */ + private static final String HOOK_FIELD = "f1"; + + /** If {@code true} put a different value. */ + @Parameterized.Parameter(0) + public Boolean differentValue; + + /** Parameters. */ + @Parameterized.Parameters(name = "differentValue = {0}") + public static Collection parameters() { + return List.of(true, false); + } + + /** {@inheritDoc} */ + @Override protected void afterTest() throws Exception { + super.afterTest(); + + stopAllGrids(); + } + + /** + * Channel failure happens after the builder has read the cached type metadata, but before the new schema is + * registered. The client must not end up with a cached type that lacks the fields of the registered schema. + * + * @throws Exception If failed. + */ + @Test + public void testChannelFailureDuringBuild() throws Exception { + IgniteEx srv = startGrid(0); + + HookIdMapper idMapper = new HookIdMapper(); + + ClientConfiguration cfg = getClientConfiguration(srv) + .setBinaryConfiguration(new BinaryConfiguration().setIdMapper(idMapper)); + + try (IgniteClient client = Ignition.startClient(cfg)) { + ClientCache cache = client.getOrCreateCache(DEFAULT_CACHE_NAME).withKeepBinary(); + + BinaryObjectBuilder builder = builder(client, FIELDS); + + // Type with all fields is registered and cached by the client. + cache.put(0, builder.build()); + + assertTrue(cache.get(0).type().fieldNames().containsAll(FIELDS)); + + idMapper.hook = () -> { + dropAllThinClientConnections(srv); + + // The next request detects the broken channel and runs the channel failure listeners. + client.cacheNames(); + }; + + if (differentValue) + builder = builder(client, FIELDS_NEW); + + cache.put(1, builder.build()); + + assertNull("Channel failure hook was not triggered", idMapper.hook); + + BinaryObject obj = cache.get(1); + + List ethalon = differentValue ? FIELDS_NEW : FIELDS; + + for (String field : ethalon) + assertEquals(field, obj.field(field)); + + assertTrue(srv.binary().type(TYPE_NAME).fieldNames().containsAll(ethalon)); + + Collection clientFields = obj.type().fieldNames(); + + assertTrue("Client type metadata lacks fields of the object schema: " + clientFields, + clientFields.containsAll(ethalon)); + } + } + + /** + * @param client Client. + * @return Builder with all fields set. + */ + private static BinaryObjectBuilder builder(IgniteClient client, List fields) { + BinaryObjectBuilder builder = client.binary().builder(TYPE_NAME); + + for (String field : fields) + builder.setField(field, field, String.class); + + return builder; + } + + /** + * ID mapper that runs a one-time hook on resolving {@link #HOOK_FIELD} ID. + * Just a simple way to reproduce concurrent disconnection during Binary Object meta processing. + */ + private static class HookIdMapper implements BinaryIdMapper { + /** Delegate. */ + private final BinaryIdMapper delegate = new BinaryBasicIdMapper(true); + + /** Hook. */ + private volatile Runnable hook; + + /** {@inheritDoc} */ + @Override public int typeId(String typeName) { + return delegate.typeId(typeName); + } + + /** {@inheritDoc} */ + @Override public int fieldId(int typeId, String fieldName) { + Runnable hook0 = hook; + + if (hook0 != null && HOOK_FIELD.equals(fieldName)) { + hook = null; + + hook0.run(); + } + + return delegate.fieldId(typeId, fieldName); + } + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteClientReconnectTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteClientReconnectTestSuite.java index fd01faeeb8553..a55e4aa26f9e8 100644 --- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteClientReconnectTestSuite.java +++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteClientReconnectTestSuite.java @@ -22,11 +22,12 @@ import org.apache.ignite.internal.IgniteClientReconnectApiExceptionTest; import org.apache.ignite.internal.IgniteClientReconnectAtomicsTest; import org.apache.ignite.internal.IgniteClientReconnectAtomicsWithLostPartitionsTest; -import org.apache.ignite.internal.IgniteClientReconnectBinaryContexTest; +import org.apache.ignite.internal.IgniteClientReconnectBinaryContextTest; import org.apache.ignite.internal.IgniteClientReconnectCacheTest; import org.apache.ignite.internal.IgniteClientReconnectCollectionsTest; import org.apache.ignite.internal.IgniteClientReconnectComputeTest; import org.apache.ignite.internal.IgniteClientReconnectContinuousProcessorTest; +import org.apache.ignite.internal.IgniteClientReconnectCustomBinaryContextTest; import org.apache.ignite.internal.IgniteClientReconnectDelayedSpiTest; import org.apache.ignite.internal.IgniteClientReconnectDeploymentTest; import org.apache.ignite.internal.IgniteClientReconnectDiscoveryStateTest; @@ -48,8 +49,9 @@ IgniteClientReconnectDiscoveryStateTest.class, IgniteClientReconnectCacheTest.class, IgniteClientReconnectDelayedSpiTest.class, - IgniteClientReconnectBinaryContexTest.class, + IgniteClientReconnectBinaryContextTest.class, IgniteClientReconnectContinuousProcessorTest.class, + IgniteClientReconnectCustomBinaryContextTest.class, IgniteClientCheckClusterGroupLocalIdAfterReconnect.class, IgniteClientReconnectComputeTest.class, IgniteClientReconnectAtomicsTest.class, diff --git a/modules/indexing/src/test/java/org/apache/ignite/client/ClientTestSuite.java b/modules/indexing/src/test/java/org/apache/ignite/client/ClientTestSuite.java index 58264b64be7e6..e708a9e88f1f8 100644 --- a/modules/indexing/src/test/java/org/apache/ignite/client/ClientTestSuite.java +++ b/modules/indexing/src/test/java/org/apache/ignite/client/ClientTestSuite.java @@ -20,6 +20,7 @@ import org.apache.ignite.client.thin.ExtraColumnInH2RowsTest; import org.apache.ignite.internal.client.thin.AffinityMetricsTest; import org.apache.ignite.internal.client.thin.AtomicLongTest; +import org.apache.ignite.internal.client.thin.BinaryMetadataChannelFailureTest; import org.apache.ignite.internal.client.thin.BlockingTxOpsTest; import org.apache.ignite.internal.client.thin.CacheAsyncTest; import org.apache.ignite.internal.client.thin.CacheConfigurationTest; @@ -115,6 +116,7 @@ ReliableChannelDuplicationTest.class, CacheExceptionsTest.class, CacheConfigurationTest.class, + BinaryMetadataChannelFailureTest.class, }) public class ClientTestSuite { // No-op. diff --git a/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java b/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java index 94a49d930a6d5..3e0a20f865d45 100644 --- a/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java +++ b/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java @@ -638,6 +638,14 @@ private class ClientBinaryMetadataHandler implements BinaryMetadataHandler { BinaryMetadata oldMeta = oldType == null ? null : ((BinaryTypeImpl)oldType).metadata(); BinaryMetadata newMeta = ((BinaryTypeImpl)meta).metadata(); + // Meta handler cache can be concurrently cleared as the result of reconnect, thus old meta - not found + // and new meta can be registered with partially filled fields during the logic below, to prevent this + // we need to request a previous version of meta and merge it if required. + if (oldType == null && newMeta.isPartialFields()) { + BinaryType reRequestedMeta = metadata(typeId); + oldMeta = reRequestedMeta != null ? ((BinaryTypeImpl)reRequestedMeta).metadata() : null; + } + // If type wasn't registered before or metadata changed, send registration request. if (oldType == null || BinaryUtils.mergeMetadata(oldMeta, newMeta) != oldMeta) { try { From b0e37764d8ce331fd3a578d3cbb9b95ebfa21aa7 Mon Sep 17 00:00:00 2001 From: zstan Date: Thu, 12 Nov 2026 17:37:40 +0300 Subject: [PATCH 2/3] revert additional test case --- .../ignite/internal/binary/BinaryContext.java | 8 - .../CacheObjectBinaryProcessorImpl.java | 2 - ...lientReconnectCustomBinaryContextTest.java | 288 ------------------ .../IgniteClientReconnectTestSuite.java | 2 - 4 files changed, 300 deletions(-) delete mode 100644 modules/core/src/test/java/org/apache/ignite/internal/IgniteClientReconnectCustomBinaryContextTest.java diff --git a/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java b/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java index 88348f652f48d..e7f8df419b022 100644 --- a/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java +++ b/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java @@ -1455,14 +1455,6 @@ BinarySchemaRegistry schemaRegistry(int typeId) { return reg; } - /** Restores preconfigured user types after reconnect. */ - public void onReconnect() { - for (BinaryClassDescriptor desc : predefinedTypes.values()) { - if (desc.userType()) - descByCls.put(desc.describedClass(), desc); - } - } - /** * Unregister all binary schemas. */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/CacheObjectBinaryProcessorImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/CacheObjectBinaryProcessorImpl.java index 30ddfe4e706f4..cab90a3f30d13 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/CacheObjectBinaryProcessorImpl.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/CacheObjectBinaryProcessorImpl.java @@ -339,8 +339,6 @@ public CacheObjectBinaryProcessorImpl(GridKernalContext ctx) { @Override public IgniteInternalFuture onReconnected(boolean clusterRestarted) throws IgniteCheckedException { reconnectFut = null; - binaryContext().onReconnect(); - return super.onReconnected(clusterRestarted); } diff --git a/modules/core/src/test/java/org/apache/ignite/internal/IgniteClientReconnectCustomBinaryContextTest.java b/modules/core/src/test/java/org/apache/ignite/internal/IgniteClientReconnectCustomBinaryContextTest.java deleted file mode 100644 index d29c7b1d2e390..0000000000000 --- a/modules/core/src/test/java/org/apache/ignite/internal/IgniteClientReconnectCustomBinaryContextTest.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * 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; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.time.Duration; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.temporal.Temporal; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicBoolean; -import org.apache.ignite.Ignite; -import org.apache.ignite.IgniteCheckedException; -import org.apache.ignite.IgniteClientDisconnectedException; -import org.apache.ignite.binary.BinaryBasicIdMapper; -import org.apache.ignite.binary.BinaryBasicNameMapper; -import org.apache.ignite.binary.BinaryObjectException; -import org.apache.ignite.binary.BinaryReader; -import org.apache.ignite.binary.BinarySerializer; -import org.apache.ignite.binary.BinaryTypeConfiguration; -import org.apache.ignite.binary.BinaryWriter; -import org.apache.ignite.cache.CacheAtomicityMode; -import org.apache.ignite.cache.CacheMode; -import org.apache.ignite.cache.CacheWriteSynchronizationMode; -import org.apache.ignite.configuration.BinaryConfiguration; -import org.apache.ignite.configuration.CacheConfiguration; -import org.apache.ignite.configuration.IgniteConfiguration; -import org.apache.ignite.spi.discovery.tcp.TcpDiscoveryIoSession; -import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi; -import org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinder; -import org.apache.ignite.spi.discovery.tcp.messages.TcpDiscoveryAbstractMessage; -import org.apache.ignite.testframework.GridTestUtils; -import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; -import org.junit.Test; -import org.springframework.util.ReflectionUtils; - -import static java.time.temporal.ChronoField.DAY_OF_MONTH; -import static java.time.temporal.ChronoField.HOUR_OF_DAY; -import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; -import static java.time.temporal.ChronoField.MONTH_OF_YEAR; -import static java.time.temporal.ChronoField.NANO_OF_SECOND; -import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; -import static java.time.temporal.ChronoField.YEAR; - -/** - * Test binary configuration restored after client reconnect. - */ -public class IgniteClientReconnectCustomBinaryContextTest extends GridCommonAbstractTest { - /** */ - public static final String CACHE_NAME = "Test"; - - /** */ - private final AtomicBoolean block = new AtomicBoolean(false); - - /** */ - @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { - IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName); - - cfg.setBinaryConfiguration( - new BinaryConfiguration() - .setTypeConfigurations( - Arrays.asList( - new BinaryTypeConfiguration(DateKey.class.getName()) - .setIdMapper(new BinaryBasicIdMapper(true)) - .setNameMapper(new BinaryBasicNameMapper(true)), - new BinaryTypeConfiguration("java.time.Ser") - .setSerializer(new LocalDateBinarySerializer()) - ) - ) - ); - - if (igniteInstanceName.equals("client")) { - TcpDiscoverySpi discoverySpi = (TcpDiscoverySpi)cfg.getDiscoverySpi(); - TcpDiscoveryIpFinder ipFinder = discoverySpi.getIpFinder(); - - BlockingDiscoverySpi clientDiscoverySpi = new BlockingDiscoverySpi(block); - clientDiscoverySpi.setIpFinder(ipFinder); - - cfg.setDiscoverySpi(clientDiscoverySpi); - - cfg.setFailureDetectionTimeout(30000); - cfg.setClientFailureDetectionTimeout(30000); - } - - return cfg; - } - - /** */ - @Test - public void testReconnect() throws Exception { - IgniteEx srv1 = startGrid(0); - IgniteEx client = startClientGrid("client"); - - srv1.getOrCreateCache(getCacheConfiguration()); - - DateKey storedKey = new DateKey(LocalDate.now()); - String val = "Hello"; - - // Put value to cache and ensure it is visible from client and server. - srv1.cache(CACHE_NAME).put(storedKey, val); - assertEquals(val, srv1.cache(CACHE_NAME).get(storedKey)); - assertEquals(val, client.cache(CACHE_NAME).get(storedKey)); - - // Wait for client disconnected. - block.set(true); - assertTrue(GridTestUtils.waitForCondition(() -> { - try { - client.cache(CACHE_NAME).get(storedKey); - } - catch (IgniteClientDisconnectedException ex) { - return true; - } - return false; - }, getTestTimeout())); - - assertTrue(client.context().clientDisconnected()); - block.set(false); - - // Wait for client to reconnect. - try { - client.cache(CACHE_NAME); - } - catch (IgniteClientDisconnectedException ex) { - ex.reconnectFuture().get(); - } - - // Ensure value is still there. - assertEquals(val, client.cache(CACHE_NAME).get(storedKey)); - - client.close(); - - Ignite client1 = startClientGrid("client"); - assertEquals(val, client1.cache(CACHE_NAME).get(storedKey)); - } - - /** */ - private static CacheConfiguration getCacheConfiguration() { - CacheConfiguration ccfg = new CacheConfiguration<>(); - - ccfg.setName(CACHE_NAME); - ccfg.setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_SYNC); - ccfg.setAtomicityMode(CacheAtomicityMode.ATOMIC); - ccfg.setTypes(DateKey.class, String.class); - ccfg.setIndexedTypes(DateKey.class, String.class); - ccfg.setCacheMode(CacheMode.PARTITIONED); - - return ccfg; - } - - /** */ - public static class DateKey { - /** */ - LocalDate date; - - /** */ - public DateKey(LocalDate date) { - this.date = date; - } - } - - /** */ - public static class LocalDateBinarySerializer implements BinarySerializer { - /** */ - private static final byte DURATION_TYPE = 1; - - /** */ - private static final byte LOCAL_DATE_TYPE = 3; - - /** */ - private static final byte LOCAL_DATE_TIME_TYPE = 5; - - /** */ - private final Field objectField; - - /** */ - private final Field typeField; - - /** */ - public LocalDateBinarySerializer() { - try { - Class cls = Class.forName("java.time.Ser"); - objectField = ReflectionUtils.findField(cls, "object"); - objectField.setAccessible(true); - - typeField = ReflectionUtils.findField(cls, "type"); - typeField.setAccessible(true); - } - catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - - /** */ - @Override public void writeBinary(Object obj, BinaryWriter writer) throws BinaryObjectException { - Byte type = (Byte)ReflectionUtils.getField(typeField, obj); - writer.writeByte("type", type); - if (type == DURATION_TYPE) { - Duration duration = (Duration)ReflectionUtils.getField(objectField, obj); - writer.writeString("duration", duration != null ? duration.toString() : null); - } - else { - Temporal date = (Temporal)ReflectionUtils.getField(objectField, obj); - writer.writeInt("year", date.get(YEAR)); - writer.writeInt("month", date.get(MONTH_OF_YEAR)); - writer.writeInt("day", date.get(DAY_OF_MONTH)); - if (type == LOCAL_DATE_TIME_TYPE) { - writer.writeInt("hour", date.get(HOUR_OF_DAY)); - writer.writeInt("minute", date.get(MINUTE_OF_HOUR)); - writer.writeInt("second", date.get(SECOND_OF_MINUTE)); - writer.writeInt("nano", date.get(NANO_OF_SECOND)); - } - } - } - - /** */ - @Override public void readBinary(Object obj, BinaryReader reader) throws BinaryObjectException { - byte type = reader.readByte("type"); - - if (type == DURATION_TYPE) { - String str = reader.readString("duration"); - Duration duration = str != null ? Duration.parse(str) : null; - ReflectionUtils.setField(objectField, obj, duration); - - } - else { - int year = reader.readInt("year"); - int month = reader.readInt("month"); - int day = reader.readInt("day"); - - LocalDate date = LocalDate.of(year, month, day); - if (type == LOCAL_DATE_TYPE) { - ReflectionUtils.setField(objectField, obj, date); - } - else { - LocalTime time = LocalTime.of( - reader.readInt("hour"), - reader.readInt("minute"), - reader.readInt("second"), - reader.readInt("nano") - ); - LocalDateTime dateTime = LocalDateTime.of(date, time); - ReflectionUtils.setField(objectField, obj, dateTime); - } - } - } - } - - /** - * Discovery SPI that stops processing messages when {@code block} is set to {@code true}. - */ - private static class BlockingDiscoverySpi extends TcpDiscoverySpi { - /** */ - private final AtomicBoolean block; - - /** */ - public BlockingDiscoverySpi(AtomicBoolean block) { - this.block = block; - } - - /** */ - @Override protected void writeMessage( - TcpDiscoveryIoSession ses, - TcpDiscoveryAbstractMessage msg, - long timeout - ) throws IOException, IgniteCheckedException { - if (!block.get()) - super.writeMessage(ses, msg, timeout); - } - } -} - diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteClientReconnectTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteClientReconnectTestSuite.java index a55e4aa26f9e8..96eaea409caef 100644 --- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteClientReconnectTestSuite.java +++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteClientReconnectTestSuite.java @@ -27,7 +27,6 @@ import org.apache.ignite.internal.IgniteClientReconnectCollectionsTest; import org.apache.ignite.internal.IgniteClientReconnectComputeTest; import org.apache.ignite.internal.IgniteClientReconnectContinuousProcessorTest; -import org.apache.ignite.internal.IgniteClientReconnectCustomBinaryContextTest; import org.apache.ignite.internal.IgniteClientReconnectDelayedSpiTest; import org.apache.ignite.internal.IgniteClientReconnectDeploymentTest; import org.apache.ignite.internal.IgniteClientReconnectDiscoveryStateTest; @@ -51,7 +50,6 @@ IgniteClientReconnectDelayedSpiTest.class, IgniteClientReconnectBinaryContextTest.class, IgniteClientReconnectContinuousProcessorTest.class, - IgniteClientReconnectCustomBinaryContextTest.class, IgniteClientCheckClusterGroupLocalIdAfterReconnect.class, IgniteClientReconnectComputeTest.class, IgniteClientReconnectAtomicsTest.class, From 952f7d1894babb6d948f8aedd321d4891779e434 Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Fri, 13 Nov 2026 09:33:24 +0300 Subject: [PATCH 3/3] IGNITE-29172 Merge new builder metadata with known type metadata instead of partialFields flag Co-Authored-By: Claude Fable 5.2 --- .../ignite/internal/binary/BinaryContext.java | 31 ++++++------- .../internal/binary/BinaryMetadata.java | 31 ------------- .../builder/BinaryObjectBuilderImpl.java | 45 +++++++------------ .../internal/client/thin/TcpIgniteClient.java | 8 ---- 4 files changed, 33 insertions(+), 82 deletions(-) diff --git a/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java b/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java index d676fba9f2960..92e5c84d9c299 100644 --- a/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java +++ b/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java @@ -1368,25 +1368,14 @@ public boolean isCompactFooter() { return compactFooter; } - /** - * Update binary meta if needed. - * - * @param writer Binary writer. - * @param meta Binary type. - * @param typeId Type ID. - * @param typeName Type name. - * @param affFieldName Affinity name. - * @param fieldsMeta Fields meta holder. - * @param partialFields It will be {@code true} if found already known object fields. - */ + /** */ public void updateMetaIfNeeded( BinaryWriterEx writer, BinaryType meta, int typeId, String typeName, String affFieldName, - Map fieldsMeta, - boolean partialFields + Map fieldsMeta ) { BinarySchemaRegistry schemaReg = schemaRegistry(typeId); @@ -1407,8 +1396,20 @@ public void updateMetaIfNeeded( registerUserClassName(typeId, typeName, writer.failIfUnregistered(), false, JAVA_ID); - updateMetadata(typeId, new BinaryMetadata(typeId, typeName, fieldsMeta, affFieldName, - Collections.singleton(curSchema), false, null, partialFields), writer.failIfUnregistered()); + BinaryMetadata newMeta = new BinaryMetadata( + typeId, + typeName, + fieldsMeta, + affFieldName, + Collections.singleton(curSchema), + false, + null + ); + + if (meta != null) + newMeta = BinaryUtils.mergeMetadata(((BinaryTypeImpl)meta).metadata(), newMeta); + + updateMetadata(typeId, newMeta, writer.failIfUnregistered()); schemaReg.addSchema(curSchema.schemaId(), curSchema); } diff --git a/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryMetadata.java b/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryMetadata.java index def287dca2be0..9fb8a918fa6fe 100644 --- a/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryMetadata.java +++ b/modules/binary/api/src/main/java/org/apache/ignite/internal/binary/BinaryMetadata.java @@ -76,10 +76,6 @@ public class BinaryMetadata implements Externalizable { /** Enum ordinal to name mapping. */ private Map ordinalToName; - /** Flag {@code true} if for optimization needs, already known object fields are not pushed into - * {@link #fields} structure. Not used during serialization. */ - private boolean partialFields; - /** * For {@link Externalizable}. */ @@ -102,27 +98,6 @@ public BinaryMetadata(int typeId, String typeName, @Nullable Map fields, - @Nullable String affKeyFieldName, @Nullable Collection schemas, boolean isEnum, - @Nullable Map enumMap, boolean partialFields) { - this(typeId, typeName, fields, affKeyFieldName, schemas, isEnum, enumMap); - - this.partialFields = partialFields; - } - /** * Constructor. * @@ -236,12 +211,6 @@ public boolean isEnum() { return isEnum; } - /** @return {@code true} if for optimization needs, already known object fields are not pushed - * into {@link #fields} structure. */ - public boolean isPartialFields() { - return partialFields; - } - /** * Wrap metadata into binary type. * diff --git a/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/builder/BinaryObjectBuilderImpl.java b/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/builder/BinaryObjectBuilderImpl.java index 307a9d2951a73..c0a66261d5b65 100644 --- a/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/builder/BinaryObjectBuilderImpl.java +++ b/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/builder/BinaryObjectBuilderImpl.java @@ -42,7 +42,6 @@ import org.apache.ignite.internal.marshaller.ClassLoaderUtils; import org.apache.ignite.internal.util.CommonUtils; import org.apache.ignite.internal.util.typedef.F; -import org.apache.ignite.internal.util.typedef.T1; import org.apache.ignite.lang.IgniteBiTuple; import org.apache.ignite.thread.IgniteThread; import org.jetbrains.annotations.Nullable; @@ -209,10 +208,6 @@ void serializeTo(BinaryWriterExImpl writer, BinaryBuilderSerializer serializer) Map fieldsMeta = null; - // For optimization needs already known object fields are not pushed into structure. - // This flag signals about such optimizations. - T1 partialFields = new T1<>(false); - if (reader != null && BinaryImplUtils.hasSchema(flags)) { Map assignedFldsById; @@ -228,7 +223,7 @@ void serializeTo(BinaryWriterExImpl writer, BinaryBuilderSerializer serializer) assignedFldsById.put(fieldId, val); if (val != REMOVED_FIELD_MARKER) - fieldsMeta = checkMetadata(meta, fieldsMeta, val, name, fieldId, partialFields); + fieldsMeta = checkMetadata(meta, fieldsMeta, val, name, fieldId); } remainsFlds = assignedFldsById.keySet(); @@ -320,10 +315,9 @@ else if (readCache == null) { serializer.writeValue(writer, val); - if (reader == null) { + if (reader == null) // Metadata has already been checked. - fieldsMeta = checkMetadata(meta, fieldsMeta, val, name, fieldId, partialFields); - } + fieldsMeta = checkMetadata(meta, fieldsMeta, val, name, fieldId); } } @@ -344,7 +338,7 @@ else if (readCache == null) { writer.postWrite(true, registeredType); - ctx.updateMetaIfNeeded(writer, meta, typeId, typeName, affFieldName, fieldsMeta, partialFields.get()); + ctx.updateMetaIfNeeded(writer, meta, typeId, typeName, affFieldName, fieldsMeta); // Update hash code after schema is written. writer.postWriteHashCode(registeredType ? null : clsNameToWrite); @@ -362,10 +356,9 @@ else if (readCache == null) { * @param newVal Field value being serialized. * @param name Field name. * @param fieldId Field ID. - * @param partialFields It will be {@code true} if found already known object fields. */ private Map checkMetadata(BinaryType meta, Map fieldsMeta, - Object newVal, String name, int fieldId, T1 partialFields) { + Object newVal, String name, int fieldId) { String oldFldTypeName = meta == null ? null : meta.fieldTypeName(name); boolean nullFieldVal = false; @@ -409,22 +402,18 @@ else if (BinaryUtils.isBinaryArray(newVal)) fieldsMeta.put(name, new BinaryFieldMetadata(newFldTypeId, fieldId)); } - else { - partialFields.set(true); - - if (!nullFieldVal) { - String newFldTypeName = BinaryUtils.fieldTypeName(newFldTypeId); - - if (!Objects.equals(newFldTypeName, oldFldTypeName) && - !oldFldTypeName.equals(BinaryUtils.fieldTypeName(GridBinaryMarshaller.OBJ))) { - throw new BinaryObjectException( - "Wrong value has been set [" + - "typeName=" + (typeName == null ? meta.typeName() : typeName) + - ", fieldName=" + name + - ", fieldType=" + oldFldTypeName + - ", assignedValueType=" + newFldTypeName + ']' - ); - } + else if (!nullFieldVal) { + String newFldTypeName = BinaryUtils.fieldTypeName(newFldTypeId); + + if (!Objects.equals(newFldTypeName, oldFldTypeName) && + !oldFldTypeName.equals(BinaryUtils.fieldTypeName(GridBinaryMarshaller.OBJ))) { + throw new BinaryObjectException( + "Wrong value has been set [" + + "typeName=" + (typeName == null ? meta.typeName() : typeName) + + ", fieldName=" + name + + ", fieldType=" + oldFldTypeName + + ", assignedValueType=" + newFldTypeName + ']' + ); } } diff --git a/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java b/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java index 3e0a20f865d45..94a49d930a6d5 100644 --- a/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java +++ b/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java @@ -638,14 +638,6 @@ private class ClientBinaryMetadataHandler implements BinaryMetadataHandler { BinaryMetadata oldMeta = oldType == null ? null : ((BinaryTypeImpl)oldType).metadata(); BinaryMetadata newMeta = ((BinaryTypeImpl)meta).metadata(); - // Meta handler cache can be concurrently cleared as the result of reconnect, thus old meta - not found - // and new meta can be registered with partially filled fields during the logic below, to prevent this - // we need to request a previous version of meta and merge it if required. - if (oldType == null && newMeta.isPartialFields()) { - BinaryType reRequestedMeta = metadata(typeId); - oldMeta = reRequestedMeta != null ? ((BinaryTypeImpl)reRequestedMeta).metadata() : null; - } - // If type wasn't registered before or metadata changed, send registration request. if (oldType == null || BinaryUtils.mergeMetadata(oldMeta, newMeta) != oldMeta) { try {