ferenc-csaky commented on code in PR #28810: URL: https://github.com/apache/flink/pull/28810#discussion_r3684947898
########## flink-core/src/test/java/org/apache/flink/api/common/typeutils/base/VariantSerializerShortReadTest.java: ########## @@ -0,0 +1,109 @@ +/* + * 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.flink.api.common.typeutils.base; + +import org.apache.flink.core.memory.DataInputViewStreamWrapper; +import org.apache.flink.core.memory.DataOutputViewStreamWrapper; +import org.apache.flink.types.variant.Variant; +import org.apache.flink.types.variant.VariantBuilder; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Regression tests for FLINK-40227: {@link VariantSerializer#deserialize} must read each of a + * Variant's value/metadata byte arrays to completion. {@code DataInputView#read(byte[])} is allowed + * to return a partial (short) read when the array straddles a buffer or compression-frame boundary + * in the source stream, and ignoring that count corrupts the Variant. This is how large Variant + * keys fail to restore from a checkpoint key-group stream on rescale (surfacing as + * MALFORMED_VARIANT or an OutOfMemoryError). + */ +class VariantSerializerShortReadTest { + + private static final VariantSerializer SERIALIZER = VariantSerializer.INSTANCE; + + /** + * A Variant with many keys so both its value and its metadata (the key dictionary) span many + * read chunks. + */ + private static Variant largeVariant() { + VariantBuilder builder = Variant.newBuilder(); + VariantBuilder.VariantObjectBuilder object = builder.object(); + for (int i = 0; i < 2048; i++) { + object.add("resource-key-" + i, builder.of("resource-value-payload-" + i)); + } + return object.build(); + } + + private static byte[] serialize(Variant variant) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + SERIALIZER.serialize(variant, new DataOutputViewStreamWrapper(out)); + return out.toByteArray(); + } + + @Test + void deserializeRecoversFromShortReads() throws IOException { + Variant original = largeVariant(); + byte[] bytes = serialize(original); + + // A source that hands back at most one byte per array read forces the short-read path on + // every value/metadata read; the stock read(byte[]) would silently drop the remainder and + // build a corrupt Variant. Review Comment: Let's remove this. This is just noise, the code is fairly straightforward. ########## flink-core/src/test/java/org/apache/flink/api/common/typeutils/base/VariantSerializerShortReadTest.java: ########## @@ -0,0 +1,109 @@ +/* + * 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.flink.api.common.typeutils.base; + +import org.apache.flink.core.memory.DataInputViewStreamWrapper; +import org.apache.flink.core.memory.DataOutputViewStreamWrapper; +import org.apache.flink.types.variant.Variant; +import org.apache.flink.types.variant.VariantBuilder; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Regression tests for FLINK-40227: {@link VariantSerializer#deserialize} must read each of a + * Variant's value/metadata byte arrays to completion. {@code DataInputView#read(byte[])} is allowed + * to return a partial (short) read when the array straddles a buffer or compression-frame boundary + * in the source stream, and ignoring that count corrupts the Variant. This is how large Variant + * keys fail to restore from a checkpoint key-group stream on rescale (surfacing as + * MALFORMED_VARIANT or an OutOfMemoryError). + */ +class VariantSerializerShortReadTest { + + private static final VariantSerializer SERIALIZER = VariantSerializer.INSTANCE; + + /** + * A Variant with many keys so both its value and its metadata (the key dictionary) span many + * read chunks. + */ + private static Variant largeVariant() { + VariantBuilder builder = Variant.newBuilder(); + VariantBuilder.VariantObjectBuilder object = builder.object(); + for (int i = 0; i < 2048; i++) { + object.add("resource-key-" + i, builder.of("resource-value-payload-" + i)); + } + return object.build(); + } + + private static byte[] serialize(Variant variant) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + SERIALIZER.serialize(variant, new DataOutputViewStreamWrapper(out)); + return out.toByteArray(); + } + + @Test + void deserializeRecoversFromShortReads() throws IOException { + Variant original = largeVariant(); + byte[] bytes = serialize(original); + + // A source that hands back at most one byte per array read forces the short-read path on + // every value/metadata read; the stock read(byte[]) would silently drop the remainder and + // build a corrupt Variant. + DataInputViewStreamWrapper source = + new DataInputViewStreamWrapper(new OneByteAtATimeInputStream(bytes)); + + Variant restored = SERIALIZER.deserialize(source); + + assertThat(restored).isEqualTo(original); + } + + @Test + void deserializeThrowsEofOnTruncatedStream() throws IOException { + byte[] bytes = serialize(largeVariant()); + byte[] truncated = Arrays.copyOf(bytes, bytes.length - 1); + + DataInputViewStreamWrapper source = + new DataInputViewStreamWrapper(new ByteArrayInputStream(truncated)); + + // A genuinely short checkpoint must fail loudly, not build a corrupt Variant from a + // partially filled buffer. Review Comment: Let's remove this. This is just noise, the code is fairly straightforward. ########## flink-core/src/test/java/org/apache/flink/api/common/typeutils/base/VariantSerializerShortReadTest.java: ########## @@ -0,0 +1,109 @@ +/* + * 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.flink.api.common.typeutils.base; + +import org.apache.flink.core.memory.DataInputViewStreamWrapper; +import org.apache.flink.core.memory.DataOutputViewStreamWrapper; +import org.apache.flink.types.variant.Variant; +import org.apache.flink.types.variant.VariantBuilder; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Regression tests for FLINK-40227: {@link VariantSerializer#deserialize} must read each of a + * Variant's value/metadata byte arrays to completion. {@code DataInputView#read(byte[])} is allowed + * to return a partial (short) read when the array straddles a buffer or compression-frame boundary + * in the source stream, and ignoring that count corrupts the Variant. This is how large Variant + * keys fail to restore from a checkpoint key-group stream on rescale (surfacing as + * MALFORMED_VARIANT or an OutOfMemoryError). + */ Review Comment: Let's make this more concise, with less implementation details about the fix, and let's remove the explicit Jira reference. For now it's obvious from the commit, and if that changes in the future, then so does this comment should be, so let's try to minimize maintenance cost in the first place. ########## flink-core/src/main/java/org/apache/flink/api/common/typeutils/base/VariantSerializer.java: ########## @@ -81,8 +81,14 @@ public Variant deserialize(DataInputView source) throws IOException { byte[] value = new byte[valueLength]; byte[] metaData = new byte[metadataLength]; - source.read(value); - source.read(metaData); + // Use readFully, not read: DataInputView#read(byte[]) may return a partial (short) read + // when the array straddles a buffer/compression-frame boundary in the source stream, and + // its returned count must not be ignored. A short read here desyncs the stream and surfaces + // as MALFORMED_VARIANT / OutOfMemoryError when restoring large Variant keys from a + // checkpoint key-group stream on rescale. readFully loops until the buffer is filled and + // throws EOFException only on a genuine end-of-stream (a truly truncated checkpoint). Review Comment: Let's make this comment more concise. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
