This is an automated email from the ASF dual-hosted git repository.
mxm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git
The following commit(s) were added to refs/heads/main by this push:
new 6383a3df85 Flink: Backport: resolving the record serializer without
loading the table (#18525) (#18781)
6383a3df85 is described below
commit 6383a3df85f3ad245b8aa921b35b68436ea6919e
Author: Swapna Marru <[email protected]>
AuthorDate: Wed Nov 4 02:52:07 2026 -0800
Flink: Backport: resolving the record serializer without loading the table
(#18525) (#18781)
---
...namicRecordSerializerDeserializerBenchmark.java | 7 +-
.../flink/sink/dynamic/DynamicIcebergSink.java | 12 +-
.../dynamic/DynamicRecordInternalSerializer.java | 26 ++---
.../sink/dynamic/DynamicRecordInternalType.java | 13 ++-
.../flink/sink/dynamic/DynamicRecordProcessor.java | 6 +-
.../flink/sink/dynamic/TableSerializerCache.java | 95 ++++++++++------
.../DynamicRecordInternalSerializerTestBase.java | 2 +-
.../TestDynamicRecordInternalSerializer.java | 52 ++++++++-
.../sink/dynamic/TestDynamicRecordProcessor.java | 2 +-
.../sink/dynamic/TestTableSerializerCache.java | 122 ++++++++++++++++++---
...namicRecordSerializerDeserializerBenchmark.java | 7 +-
.../flink/sink/dynamic/DynamicIcebergSink.java | 12 +-
.../dynamic/DynamicRecordInternalSerializer.java | 26 ++---
.../sink/dynamic/DynamicRecordInternalType.java | 10 +-
.../flink/sink/dynamic/DynamicRecordProcessor.java | 6 +-
.../flink/sink/dynamic/TableSerializerCache.java | 95 ++++++++++------
.../DynamicRecordInternalSerializerTestBase.java | 2 +-
.../TestDynamicRecordInternalSerializer.java | 52 ++++++++-
.../sink/dynamic/TestDynamicRecordProcessor.java | 2 +-
.../sink/dynamic/TestTableSerializerCache.java | 122 ++++++++++++++++++---
20 files changed, 528 insertions(+), 143 deletions(-)
diff --git
a/flink/v1.20/flink/src/jmh/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordSerializerDeserializerBenchmark.java
b/flink/v1.20/flink/src/jmh/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordSerializerDeserializerBenchmark.java
index 6e5d04499e..3fe18a6a0d 100644
---
a/flink/v1.20/flink/src/jmh/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordSerializerDeserializerBenchmark.java
+++
b/flink/v1.20/flink/src/jmh/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordSerializerDeserializerBenchmark.java
@@ -110,7 +110,12 @@ public class DynamicRecordSerializerDeserializerBenchmark {
"hadoop",
new Configuration(),
ImmutableMap.of(CatalogProperties.WAREHOUSE_LOCATION,
warehouse.getPath()));
- this.type = new DynamicRecordInternalType(catalogLoader, true, 100);
+ this.type =
+ new DynamicRecordInternalType(
+ catalogLoader,
+ true,
+ 100,
+
FlinkDynamicSinkOptions.INPUT_SCHEMAS_PER_TABLE_CACHE_MAX_SIZE.defaultValue());
}
@Benchmark
diff --git
a/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicIcebergSink.java
b/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicIcebergSink.java
index eab3d81c44..42c52894ad 100644
---
a/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicIcebergSink.java
+++
b/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicIcebergSink.java
@@ -522,9 +522,17 @@ public class DynamicIcebergSink
Configuration flinkConfig = fromReadableConfig();
DynamicRecordInternalType type =
- new DynamicRecordInternalType(catalogLoader, false,
flinkDynamicSinkConf.cacheMaxSize());
+ new DynamicRecordInternalType(
+ catalogLoader,
+ false,
+ flinkDynamicSinkConf.cacheMaxSize(),
+ flinkDynamicSinkConf.inputSchemasPerTableCacheMaxSize());
DynamicRecordInternalType sideOutputType =
- new DynamicRecordInternalType(catalogLoader, true,
flinkDynamicSinkConf.cacheMaxSize());
+ new DynamicRecordInternalType(
+ catalogLoader,
+ true,
+ flinkDynamicSinkConf.cacheMaxSize(),
+ flinkDynamicSinkConf.inputSchemasPerTableCacheMaxSize());
SingleOutputStreamOperator<DynamicRecordInternal> converted =
input
diff --git
a/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializer.java
b/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializer.java
index e9e7daef46..b7d777946d 100644
---
a/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializer.java
+++
b/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializer.java
@@ -65,7 +65,10 @@ class DynamicRecordInternalSerializer extends
TypeSerializer<DynamicRecordIntern
@Override
public TypeSerializer<DynamicRecordInternal> duplicate() {
return new DynamicRecordInternalSerializer(
- new TableSerializerCache(serializerCache.catalogLoader(),
serializerCache.maximumSize()),
+ new TableSerializerCache(
+ serializerCache.catalogLoader(),
+ serializerCache.maximumSize(),
+ serializerCache.inputSchemasPerTableCacheMaximumSize()),
writeSchemaAndSpec,
writeLongUTF);
}
@@ -96,17 +99,10 @@ class DynamicRecordInternalSerializer extends
TypeSerializer<DynamicRecordIntern
dataOutputView.writeInt(toSerialize.writerKey());
final RowDataSerializer rowDataSerializer;
if (writeSchemaAndSpec) {
- rowDataSerializer =
- serializerCache.serializer(
- toSerialize.tableName(), toSerialize.schema(),
toSerialize.spec());
+ rowDataSerializer = serializerCache.serializer(toSerialize.tableName(),
toSerialize.schema());
} else {
- // Check that the schema id can be resolved. Not strictly necessary for
serialization.
- Tuple3<RowDataSerializer, Schema, PartitionSpec> serializer =
- serializerCache.serializerWithSchemaAndSpec(
- toSerialize.tableName(),
- toSerialize.schema().schemaId(),
- toSerialize.spec().specId());
- rowDataSerializer = serializer.f0;
+ rowDataSerializer =
+ serializerCache.serializerById(toSerialize.tableName(),
toSerialize.schema());
}
rowDataSerializer.serialize(toSerialize.rowData(), dataOutputView);
@@ -133,7 +129,7 @@ class DynamicRecordInternalSerializer extends
TypeSerializer<DynamicRecordIntern
}
spec = PartitionSpecParser.fromJson(schema, dataInputView.readUTF());
- rowDataSerializer = serializerCache.serializer(tableName, schema, spec);
+ rowDataSerializer = serializerCache.serializer(tableName, schema);
} else {
Integer schemaId = dataInputView.readInt();
Integer specId = dataInputView.readInt();
@@ -184,7 +180,7 @@ class DynamicRecordInternalSerializer extends
TypeSerializer<DynamicRecordIntern
spec = PartitionSpecParser.fromJson(schema, dataInputView.readUTF());
reuse.setSchema(schema);
reuse.setSpec(spec);
- rowDataSerializer = serializerCache.serializer(tableName, schema, spec);
+ rowDataSerializer = serializerCache.serializer(tableName, schema);
} else {
Integer schemaId = dataInputView.readInt();
Integer specId = dataInputView.readInt();
@@ -216,7 +212,7 @@ class DynamicRecordInternalSerializer extends
TypeSerializer<DynamicRecordIntern
@Override
public DynamicRecordInternal copy(DynamicRecordInternal from) {
RowDataSerializer rowDataSerializer =
- serializerCache.serializer(from.tableName(), from.schema(),
from.spec());
+ serializerCache.serializer(from.tableName(), from.schema());
return new DynamicRecordInternal(
from.tableName(),
from.branch(),
@@ -231,7 +227,7 @@ class DynamicRecordInternalSerializer extends
TypeSerializer<DynamicRecordIntern
@Override
public DynamicRecordInternal copy(DynamicRecordInternal from,
DynamicRecordInternal reuse) {
RowDataSerializer rowDataSerializer =
- serializerCache.serializer(from.tableName(), from.schema(),
from.spec());
+ serializerCache.serializer(from.tableName(), from.schema());
reuse.setTableName(from.tableName());
reuse.setBranch(from.branch());
reuse.setSchema(from.schema());
diff --git
a/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalType.java
b/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalType.java
index 6be081aadf..b23db385b1 100644
---
a/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalType.java
+++
b/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalType.java
@@ -31,12 +31,17 @@ class DynamicRecordInternalType extends
TypeInformation<DynamicRecordInternal> {
private final CatalogLoader catalogLoader;
private final boolean writeSchemaAndSpec;
private final int cacheSize;
+ private final int inputSchemasPerTableCacheMaximumSize;
DynamicRecordInternalType(
- CatalogLoader catalogLoader, boolean writeSchemaAndSpec, int cacheSize) {
+ CatalogLoader catalogLoader,
+ boolean writeSchemaAndSpec,
+ int cacheSize,
+ int inputSchemasPerTableCacheMaximumSize) {
this.catalogLoader = catalogLoader;
this.writeSchemaAndSpec = writeSchemaAndSpec;
this.cacheSize = cacheSize;
+ this.inputSchemasPerTableCacheMaximumSize =
inputSchemasPerTableCacheMaximumSize;
}
@Override
@@ -72,13 +77,15 @@ class DynamicRecordInternalType extends
TypeInformation<DynamicRecordInternal> {
@Override
public TypeSerializer<DynamicRecordInternal>
createSerializer(SerializerConfig serializerConfig) {
return new DynamicRecordInternalSerializer(
- new TableSerializerCache(catalogLoader, cacheSize),
writeSchemaAndSpec);
+ new TableSerializerCache(catalogLoader, cacheSize,
inputSchemasPerTableCacheMaximumSize),
+ writeSchemaAndSpec);
}
@Override
public TypeSerializer<DynamicRecordInternal>
createSerializer(ExecutionConfig executionConfig) {
return new DynamicRecordInternalSerializer(
- new TableSerializerCache(catalogLoader, cacheSize),
writeSchemaAndSpec);
+ new TableSerializerCache(catalogLoader, cacheSize,
inputSchemasPerTableCacheMaximumSize),
+ writeSchemaAndSpec);
}
@Override
diff --git
a/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordProcessor.java
b/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordProcessor.java
index ba2d816ae9..91f5d52b02 100644
---
a/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordProcessor.java
+++
b/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordProcessor.java
@@ -104,12 +104,14 @@ class DynamicRecordProcessor<T> extends
ProcessFunction<T, DynamicRecordInternal
this.forwardStream =
new OutputTag<>(
DYNAMIC_FORWARD_STREAM,
- new DynamicRecordInternalType(catalogLoader, true,
cacheMaximumSize)) {};
+ new DynamicRecordInternalType(
+ catalogLoader, true, cacheMaximumSize,
inputSchemasPerTableCacheMaximumSize)) {};
if (!immediateUpdate) {
updateStream =
new OutputTag<>(
DYNAMIC_TABLE_UPDATE_STREAM,
- new DynamicRecordInternalType(catalogLoader, true,
cacheMaximumSize)) {};
+ new DynamicRecordInternalType(
+ catalogLoader, true, cacheMaximumSize,
inputSchemasPerTableCacheMaximumSize)) {};
}
this.dynamicRecordWithConfig =
diff --git
a/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/TableSerializerCache.java
b/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/TableSerializerCache.java
index 13b4fa56c9..e7b37c391a 100644
---
a/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/TableSerializerCache.java
+++
b/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/TableSerializerCache.java
@@ -20,7 +20,6 @@ package org.apache.iceberg.flink.sink.dynamic;
import java.io.Serializable;
import java.util.Map;
-import javax.annotation.Nullable;
import org.apache.flink.annotation.Internal;
import org.apache.flink.annotation.VisibleForTesting;
import org.apache.flink.api.java.tuple.Tuple3;
@@ -51,43 +50,41 @@ class TableSerializerCache implements Serializable {
private final CatalogLoader catalogLoader;
private final int maximumSize;
+ private final int inputSchemasPerTableCacheMaximumSize;
// Intentionally not closed; the catalog is reused for the serializer's
lifetime.
private transient Catalog catalog;
private transient Map<String, SerializerInfo> serializers;
- TableSerializerCache(CatalogLoader catalogLoader, int maximumSize) {
+ TableSerializerCache(
+ CatalogLoader catalogLoader, int maximumSize, int
inputSchemasPerTableCacheMaximumSize) {
this.catalogLoader = catalogLoader;
this.maximumSize = maximumSize;
+ this.inputSchemasPerTableCacheMaximumSize =
inputSchemasPerTableCacheMaximumSize;
}
- RowDataSerializer serializer(String tableName, Schema schema, PartitionSpec
spec) {
- return serializer(tableName, schema, spec, null, null).f0;
+ RowDataSerializer serializer(String tableName, Schema schema) {
+ return serializerInfo(tableName).serializerBySchema(schema);
}
- Tuple3<RowDataSerializer, Schema, PartitionSpec> serializerWithSchemaAndSpec(
- String tableName, Integer schemaId, Integer specId) {
- return serializer(tableName, null, null, schemaId, specId);
+ /**
+ * Resolves the serializer when the caller already holds the schema and spec
along with their ids.
+ * The serializer is looked up by schema id, so it is not rebuilt when the
{@link Schema} instance
+ * changes.
+ *
+ * <p>The id-keyed serializers are shared with {@link
#serializerWithSchemaAndSpec}, so the schema
+ * must come from the table. Do not use this for user-supplied schemas,
whose ids are not reliable
+ * and could collide with the table's schemas.
+ */
+ RowDataSerializer serializerById(String tableName, Schema schema) {
+ return serializerInfo(tableName).serializerBySchemaId(schema);
}
- private Tuple3<RowDataSerializer, Schema, PartitionSpec> serializer(
- String tableName,
- @Nullable Schema unknownSchema,
- @Nullable PartitionSpec unknownSpec,
- @Nullable Integer schemaId,
- @Nullable Integer specId) {
- Preconditions.checkState(
- (unknownSchema == null && unknownSpec == null) ^ (schemaId == null &&
specId == null),
- "Either the full schema/spec or their ids must be provided.");
-
- if (serializers == null) {
- // We need to initialize the cache at the first time
- this.serializers = new LRUCache<>(maximumSize);
- }
-
- SerializerInfo info = serializers.computeIfAbsent(tableName,
SerializerInfo::new);
- Schema schema = unknownSchema != null ? unknownSchema :
info.schemas.get(schemaId);
- PartitionSpec spec = unknownSpec != null ? unknownSpec :
info.specs.get(specId);
+ Tuple3<RowDataSerializer, Schema, PartitionSpec> serializerWithSchemaAndSpec(
+ String tableName, Integer schemaId, Integer specId) {
+ SerializerInfo info = serializerInfo(tableName);
+ Schema schema = info.schemas.get(schemaId);
+ PartitionSpec spec = info.specs.get(specId);
if (schema == null || spec == null) {
info.update();
@@ -95,11 +92,28 @@ class TableSerializerCache implements Serializable {
spec = info.specs.get(specId);
}
- RowDataSerializer serializer =
- info.serializers.computeIfAbsent(
- schema, s -> new RowDataSerializer(FlinkSchemaUtil.convert(s)));
+ Preconditions.checkArgument(
+ schema != null,
+ "SchemaId:%s not found in table %s. Expected SchemaId:%s to exist.",
+ schemaId,
+ tableName,
+ schemaId);
+ Preconditions.checkArgument(
+ spec != null,
+ "SpecId:%s not found in table %s. Expected SpecId:%s to exist.",
+ specId,
+ tableName,
+ specId);
+
+ return Tuple3.of(info.serializerBySchemaId(schema), schema, spec);
+ }
+
+ private SerializerInfo serializerInfo(String tableName) {
+ if (serializers == null) {
+ this.serializers = new LRUCache<>(maximumSize);
+ }
- return Tuple3.of(serializer, schema, spec);
+ return serializers.computeIfAbsent(tableName, SerializerInfo::new);
}
CatalogLoader catalogLoader() {
@@ -110,19 +124,38 @@ class TableSerializerCache implements Serializable {
return maximumSize;
}
+ int inputSchemasPerTableCacheMaximumSize() {
+ return inputSchemasPerTableCacheMaximumSize;
+ }
+
private class SerializerInfo {
private final String tableName;
- private final Map<Schema, RowDataSerializer> serializers;
+ private final Map<Schema, RowDataSerializer> serializersBySchema;
+ private final Map<Integer, RowDataSerializer> serializersBySchemaId;
private Map<Integer, Schema> schemas;
private Map<Integer, PartitionSpec> specs;
SerializerInfo(String tableName) {
this.tableName = tableName;
- this.serializers = Maps.newHashMapWithExpectedSize(2);
+ this.serializersBySchema = new
LRUCache<>(inputSchemasPerTableCacheMaximumSize);
+ this.serializersBySchemaId = Maps.newHashMapWithExpectedSize(2);
this.schemas = Maps.newHashMapWithExpectedSize(1);
this.specs = Maps.newHashMapWithExpectedSize(0);
}
+ private RowDataSerializer serializerBySchema(Schema schema) {
+ return serializersBySchema.computeIfAbsent(schema,
this::createSerializer);
+ }
+
+ private RowDataSerializer serializerBySchemaId(Schema schema) {
+ return serializersBySchemaId.computeIfAbsent(
+ schema.schemaId(), id -> createSerializer(schema));
+ }
+
+ private RowDataSerializer createSerializer(Schema schema) {
+ return new RowDataSerializer(FlinkSchemaUtil.convert(schema));
+ }
+
private void update() {
if (catalog == null) {
catalog = catalogLoader.loadCatalog();
diff --git
a/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializerTestBase.java
b/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializerTestBase.java
index 07096b891a..07a2ceca99 100644
---
a/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializerTestBase.java
+++
b/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializerTestBase.java
@@ -65,7 +65,7 @@ abstract class DynamicRecordInternalSerializerTestBase
@Override
protected TypeSerializer<DynamicRecordInternal> createSerializer() {
return new DynamicRecordInternalSerializer(
- new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 1),
+ new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 1, 10),
writeFullSchemaAndSpec,
writeLongUTF);
}
diff --git
a/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordInternalSerializer.java
b/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordInternalSerializer.java
index 5667583599..d24076c382 100644
---
a/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordInternalSerializer.java
+++
b/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordInternalSerializer.java
@@ -20,15 +20,18 @@ package org.apache.iceberg.flink.sink.dynamic;
import static org.apache.iceberg.types.Types.NestedField.required;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
import java.io.IOException;
import java.util.Collections;
+import org.apache.flink.api.common.serialization.SerializerConfigImpl;
import org.apache.flink.api.common.typeutils.TypeSerializer;
import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
import org.apache.flink.core.memory.DataInputDeserializer;
import org.apache.flink.core.memory.DataOutputSerializer;
import org.apache.flink.table.data.GenericRowData;
import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
import org.apache.flink.types.RowKind;
import org.apache.iceberg.PartitionSpec;
import org.apache.iceberg.Schema;
@@ -107,6 +110,53 @@ public class TestDynamicRecordInternalSerializer {
.isTrue();
}
+ @Test
+ void testSerializeWithSchemaIdForMissingTable() {
+ DynamicRecordInternalSerializer serializer =
+ new DynamicRecordInternalSerializer(
+ new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 1,
10), false);
+ GenericRowData row = GenericRowData.of(123L,
StringData.fromString("test"), 1.23f);
+ DynamicRecordInternal record =
+ new DynamicRecordInternal(
+ "missingTable",
+ DynamicRecordInternalSerializerTestBase.BRANCH,
+ DynamicRecordInternalSerializerTestBase.SCHEMA,
+ row,
+ DynamicRecordInternalSerializerTestBase.SPEC,
+ 42,
+ false,
+ Collections.emptySet());
+ DataOutputSerializer output = new DataOutputSerializer(128);
+
+ assertThatCode(() -> serializer.serialize(record,
output)).doesNotThrowAnyException();
+ assertThat(output.length()).isGreaterThan(0);
+ }
+
+ @Test
+ void testCacheSizesFromType() {
+ DynamicRecordInternalType type =
+ new DynamicRecordInternalType(CATALOG_EXTENSION.catalogLoader(), true,
5, 3);
+ TypeSerializer<DynamicRecordInternal> internalTypeSerializer =
+ type.createSerializer(new SerializerConfigImpl());
+
assertThat(internalTypeSerializer).isInstanceOf(DynamicRecordInternalSerializer.class);
+ DynamicRecordInternalSerializer dynamicRecordInternalSerializer =
+ (DynamicRecordInternalSerializer) internalTypeSerializer;
+
+
assertThat(dynamicRecordInternalSerializer.getSerializerCache().maximumSize()).isEqualTo(5);
+ assertThat(
+ dynamicRecordInternalSerializer
+ .getSerializerCache()
+ .inputSchemasPerTableCacheMaximumSize())
+ .isEqualTo(3);
+
+ DynamicRecordInternalSerializer duplicate =
+ (DynamicRecordInternalSerializer)
dynamicRecordInternalSerializer.duplicate();
+ assertThat(duplicate.getSerializerCache())
+ .isNotSameAs(dynamicRecordInternalSerializer.getSerializerCache());
+ assertThat(duplicate.getSerializerCache().maximumSize()).isEqualTo(5);
+
assertThat(duplicate.getSerializerCache().inputSchemasPerTableCacheMaximumSize()).isEqualTo(3);
+ }
+
@ParameterizedTest
@ValueSource(booleans = {false, true})
void copyPreservesValues(boolean reuse) {
@@ -167,7 +217,7 @@ public class TestDynamicRecordInternalSerializer {
private DynamicRecordInternalSerializer createSerializer() {
return new DynamicRecordInternalSerializer(
- new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 1), true);
+ new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 1, 10),
true);
}
private static class OldTypeSerializerSnapshot
diff --git
a/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordProcessor.java
b/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordProcessor.java
index 48b94c14c7..6f50e62f2c 100644
---
a/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordProcessor.java
+++
b/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordProcessor.java
@@ -179,7 +179,7 @@ class TestDynamicRecordProcessor {
return new OutputTag<>(
DynamicRecordProcessor.DYNAMIC_TABLE_UPDATE_STREAM,
new DynamicRecordInternalType(
- CATALOG_EXTENSION.catalogLoader(), true, CACHE_REFRESH_MS)) {};
+ CATALOG_EXTENSION.catalogLoader(), true, CACHE_REFRESH_MS, 10)) {};
}
private static DynamicRecord recordWithDistributionMode(
diff --git
a/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestTableSerializerCache.java
b/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestTableSerializerCache.java
index bbfb39618f..156ec0def0 100644
---
a/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestTableSerializerCache.java
+++
b/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestTableSerializerCache.java
@@ -24,6 +24,7 @@ import static
org.apache.iceberg.types.Types.NestedField.optional;
import static org.apache.iceberg.types.Types.NestedField.required;
import static org.apache.iceberg.types.Types.StringType;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.function.Supplier;
import org.apache.flink.api.java.tuple.Tuple3;
@@ -52,16 +53,13 @@ class TestTableSerializerCache {
optional(2, "data", StringType.get()),
optional(3, "double", DoubleType.get()));
- TableSerializerCache cache = new
TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 10);
+ TableSerializerCache cache = new
TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 10, 10);
@Test
void testFullSchemaCaching() {
- Supplier<RowDataSerializer> creator1a =
- () -> cache.serializer("table", schema1,
PartitionSpec.unpartitioned());
- Supplier<RowDataSerializer> creator1b =
- () -> cache.serializer("table", schema2,
PartitionSpec.unpartitioned());
- Supplier<RowDataSerializer> creator2 =
- () -> cache.serializer("table2", schema2,
PartitionSpec.unpartitioned());
+ Supplier<RowDataSerializer> creator1a = () -> cache.serializer("table",
schema1);
+ Supplier<RowDataSerializer> creator1b = () -> cache.serializer("table",
schema2);
+ Supplier<RowDataSerializer> creator2 = () -> cache.serializer("table2",
schema2);
RowDataSerializer serializer1a = creator1a.get();
RowDataSerializer serializer1b = creator1b.get();
@@ -73,10 +71,32 @@ class TestTableSerializerCache {
assertThat(serializer2).isSameAs(creator2.get());
}
+ @Test
+ void testSerializerById() {
+ LoadCountingCatalogLoader catalogLoader =
+ new LoadCountingCatalogLoader(CATALOG_EXTENSION.catalogLoader());
+ cache = new TableSerializerCache(catalogLoader, 10, 10);
+
+ Schema schema1Copy = new Schema(schema1.schemaId(), required(1, "id",
LongType.get()));
+ Supplier<RowDataSerializer> creator1a = () ->
cache.serializerById("table", schema1);
+ Supplier<RowDataSerializer> creator1b = () ->
cache.serializerById("table", schema1Copy);
+ Supplier<RowDataSerializer> creator2 = () -> cache.serializerById("table",
schema2);
+ Supplier<RowDataSerializer> creator3 = () ->
cache.serializerById("table2", schema1);
+
+ RowDataSerializer serializer1a = creator1a.get();
+ RowDataSerializer serializer1b = creator1b.get();
+ RowDataSerializer serializer2 = creator2.get();
+ RowDataSerializer serializer3 = creator3.get();
+
+ assertThat(serializer1a).isNotSameAs(serializer2).isNotSameAs(serializer3);
+ assertThat(serializer1b).isSameAs(serializer1a);
+ assertThat(catalogLoader.loadCount()).isEqualTo(0);
+ }
+
@Test
void testCachingWithSchemaLookup() {
CatalogLoader catalogLoader = CATALOG_EXTENSION.catalogLoader();
- cache = new TableSerializerCache(catalogLoader, 10);
+ cache = new TableSerializerCache(catalogLoader, 10, 10);
Catalog catalog = catalogLoader.loadCatalog();
Table table = catalog.createTable(TableIdentifier.of("table"), schema1);
@@ -98,15 +118,87 @@ class TestTableSerializerCache {
assertThat(serializerWithSchemaAndSpec.f2).isSameAs(serializerWithSchemaAndSpec2.f2);
}
+ @Test
+ void testSchemaLookupWithSchemaEvolution() {
+ Table table =
CATALOG_EXTENSION.catalog().createTable(TableIdentifier.of("table"), schema1);
+ cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 10,
10);
+ int specId = table.spec().specId();
+ int schemaId1 = table.schema().schemaId();
+ Supplier<RowDataSerializer> creator1a =
+ () -> cache.serializerWithSchemaAndSpec("table", schemaId1, specId).f0;
+
+ table.updateSchema().addColumn("data", StringType.get()).commit();
+
+ int schemaId2 = table.schema().schemaId();
+ Supplier<RowDataSerializer> creator2 =
+ () -> cache.serializerWithSchemaAndSpec("table", schemaId2, specId).f0;
+ Supplier<RowDataSerializer> creator1b =
+ () -> cache.serializerWithSchemaAndSpec("table", schemaId1, specId).f0;
+
+ assertThat(creator2.get()).isNotSameAs(creator1a.get());
+ assertThat(creator1b.get()).isSameAs(creator1a.get());
+ }
+
+ @Test
+ void testSchemaSerializerEviction() {
+ cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 10, 1);
+ Supplier<RowDataSerializer> creator1 = () -> cache.serializer("table",
schema1);
+ Supplier<RowDataSerializer> creator2 = () -> cache.serializer("table",
schema2);
+
+ RowDataSerializer serializer1 = creator1.get();
+ assertThat(creator1.get()).isSameAs(serializer1);
+
+ // Only one schema fits, so caching schema2 evicts the serializer for
schema1
+ RowDataSerializer serializer2 = creator2.get();
+ assertThat(creator2.get()).isSameAs(serializer2);
+ assertThat(creator1.get()).isNotSameAs(serializer1);
+ }
+
+ @Test
+ void testSerializerByIdSharedWithSchemaLookup() {
+ Table table =
CATALOG_EXTENSION.catalog().createTable(TableIdentifier.of("table"), schema1);
+ cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 10,
10);
+ int schemaId = table.schema().schemaId();
+ int specId = table.spec().specId();
+ Supplier<RowDataSerializer> creator1 = () -> cache.serializerById("table",
table.schema());
+ Supplier<RowDataSerializer> creator2 =
+ () -> cache.serializerWithSchemaAndSpec("table", schemaId, specId).f0;
+
+ RowDataSerializer serializer1 = creator1.get();
+
+ assertThat(creator2.get()).isSameAs(serializer1);
+ assertThat(creator1.get()).isSameAs(serializer1);
+ }
+
+ @Test
+ void testSchemaLookupWithUnknownSchemaId() {
+ Table table =
CATALOG_EXTENSION.catalog().createTable(TableIdentifier.of("table"), schema1);
+ cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 10,
10);
+ int specId = table.spec().specId();
+
+ assertThatThrownBy(() -> cache.serializerWithSchemaAndSpec("table", 999,
specId))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("SchemaId:999 not found in table table. Expected
SchemaId:999 to exist.");
+ }
+
+ @Test
+ void testSchemaLookupWithUnknownSpecId() {
+ Table table =
CATALOG_EXTENSION.catalog().createTable(TableIdentifier.of("table"), schema1);
+ cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 10,
10);
+ int schemaId = table.schema().schemaId();
+
+ assertThatThrownBy(() -> cache.serializerWithSchemaAndSpec("table",
schemaId, 999))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("SpecId:999 not found in table table. Expected SpecId:999
to exist.");
+ }
+
@Test
void testCacheEviction() {
- cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 0);
+ cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 0, 10);
assertThat(cache.maximumSize()).isEqualTo(0);
- Supplier<RowDataSerializer> creator1 =
- () -> cache.serializer("table", schema1,
PartitionSpec.unpartitioned());
- Supplier<RowDataSerializer> creator2 =
- () -> cache.serializer("table2", schema2,
PartitionSpec.unpartitioned());
+ Supplier<RowDataSerializer> creator1 = () -> cache.serializer("table",
schema1);
+ Supplier<RowDataSerializer> creator2 = () -> cache.serializer("table2",
schema2);
RowDataSerializer serializer1 = creator1.get();
RowDataSerializer serializer2 = creator2.get();
@@ -118,7 +210,7 @@ class TestTableSerializerCache {
@Test
void testCacheSize() {
- cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 1000);
+ cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 1000,
10);
assertThat(cache.maximumSize()).isEqualTo(1000);
}
@@ -129,7 +221,7 @@ class TestTableSerializerCache {
LoadCountingCatalogLoader catalogLoader =
new LoadCountingCatalogLoader(CATALOG_EXTENSION.catalogLoader());
- cache = new TableSerializerCache(catalogLoader, 10);
+ cache = new TableSerializerCache(catalogLoader, 10, 10);
// schema/spec ids are unknown, so both lookups miss the cache and need
the catalog
cache.serializerWithSchemaAndSpec(
diff --git
a/flink/v2.2/flink/src/jmh/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordSerializerDeserializerBenchmark.java
b/flink/v2.2/flink/src/jmh/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordSerializerDeserializerBenchmark.java
index 6e5d04499e..3fe18a6a0d 100644
---
a/flink/v2.2/flink/src/jmh/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordSerializerDeserializerBenchmark.java
+++
b/flink/v2.2/flink/src/jmh/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordSerializerDeserializerBenchmark.java
@@ -110,7 +110,12 @@ public class DynamicRecordSerializerDeserializerBenchmark {
"hadoop",
new Configuration(),
ImmutableMap.of(CatalogProperties.WAREHOUSE_LOCATION,
warehouse.getPath()));
- this.type = new DynamicRecordInternalType(catalogLoader, true, 100);
+ this.type =
+ new DynamicRecordInternalType(
+ catalogLoader,
+ true,
+ 100,
+
FlinkDynamicSinkOptions.INPUT_SCHEMAS_PER_TABLE_CACHE_MAX_SIZE.defaultValue());
}
@Benchmark
diff --git
a/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicIcebergSink.java
b/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicIcebergSink.java
index c8f9fece23..3d478ecd6f 100644
---
a/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicIcebergSink.java
+++
b/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicIcebergSink.java
@@ -520,9 +520,17 @@ public class DynamicIcebergSink
Configuration flinkConfig = fromReadableConfig();
DynamicRecordInternalType type =
- new DynamicRecordInternalType(catalogLoader, false,
flinkDynamicSinkConf.cacheMaxSize());
+ new DynamicRecordInternalType(
+ catalogLoader,
+ false,
+ flinkDynamicSinkConf.cacheMaxSize(),
+ flinkDynamicSinkConf.inputSchemasPerTableCacheMaxSize());
DynamicRecordInternalType sideOutputType =
- new DynamicRecordInternalType(catalogLoader, true,
flinkDynamicSinkConf.cacheMaxSize());
+ new DynamicRecordInternalType(
+ catalogLoader,
+ true,
+ flinkDynamicSinkConf.cacheMaxSize(),
+ flinkDynamicSinkConf.inputSchemasPerTableCacheMaxSize());
SingleOutputStreamOperator<DynamicRecordInternal> converted =
input
diff --git
a/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializer.java
b/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializer.java
index e9e7daef46..b7d777946d 100644
---
a/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializer.java
+++
b/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializer.java
@@ -65,7 +65,10 @@ class DynamicRecordInternalSerializer extends
TypeSerializer<DynamicRecordIntern
@Override
public TypeSerializer<DynamicRecordInternal> duplicate() {
return new DynamicRecordInternalSerializer(
- new TableSerializerCache(serializerCache.catalogLoader(),
serializerCache.maximumSize()),
+ new TableSerializerCache(
+ serializerCache.catalogLoader(),
+ serializerCache.maximumSize(),
+ serializerCache.inputSchemasPerTableCacheMaximumSize()),
writeSchemaAndSpec,
writeLongUTF);
}
@@ -96,17 +99,10 @@ class DynamicRecordInternalSerializer extends
TypeSerializer<DynamicRecordIntern
dataOutputView.writeInt(toSerialize.writerKey());
final RowDataSerializer rowDataSerializer;
if (writeSchemaAndSpec) {
- rowDataSerializer =
- serializerCache.serializer(
- toSerialize.tableName(), toSerialize.schema(),
toSerialize.spec());
+ rowDataSerializer = serializerCache.serializer(toSerialize.tableName(),
toSerialize.schema());
} else {
- // Check that the schema id can be resolved. Not strictly necessary for
serialization.
- Tuple3<RowDataSerializer, Schema, PartitionSpec> serializer =
- serializerCache.serializerWithSchemaAndSpec(
- toSerialize.tableName(),
- toSerialize.schema().schemaId(),
- toSerialize.spec().specId());
- rowDataSerializer = serializer.f0;
+ rowDataSerializer =
+ serializerCache.serializerById(toSerialize.tableName(),
toSerialize.schema());
}
rowDataSerializer.serialize(toSerialize.rowData(), dataOutputView);
@@ -133,7 +129,7 @@ class DynamicRecordInternalSerializer extends
TypeSerializer<DynamicRecordIntern
}
spec = PartitionSpecParser.fromJson(schema, dataInputView.readUTF());
- rowDataSerializer = serializerCache.serializer(tableName, schema, spec);
+ rowDataSerializer = serializerCache.serializer(tableName, schema);
} else {
Integer schemaId = dataInputView.readInt();
Integer specId = dataInputView.readInt();
@@ -184,7 +180,7 @@ class DynamicRecordInternalSerializer extends
TypeSerializer<DynamicRecordIntern
spec = PartitionSpecParser.fromJson(schema, dataInputView.readUTF());
reuse.setSchema(schema);
reuse.setSpec(spec);
- rowDataSerializer = serializerCache.serializer(tableName, schema, spec);
+ rowDataSerializer = serializerCache.serializer(tableName, schema);
} else {
Integer schemaId = dataInputView.readInt();
Integer specId = dataInputView.readInt();
@@ -216,7 +212,7 @@ class DynamicRecordInternalSerializer extends
TypeSerializer<DynamicRecordIntern
@Override
public DynamicRecordInternal copy(DynamicRecordInternal from) {
RowDataSerializer rowDataSerializer =
- serializerCache.serializer(from.tableName(), from.schema(),
from.spec());
+ serializerCache.serializer(from.tableName(), from.schema());
return new DynamicRecordInternal(
from.tableName(),
from.branch(),
@@ -231,7 +227,7 @@ class DynamicRecordInternalSerializer extends
TypeSerializer<DynamicRecordIntern
@Override
public DynamicRecordInternal copy(DynamicRecordInternal from,
DynamicRecordInternal reuse) {
RowDataSerializer rowDataSerializer =
- serializerCache.serializer(from.tableName(), from.schema(),
from.spec());
+ serializerCache.serializer(from.tableName(), from.schema());
reuse.setTableName(from.tableName());
reuse.setBranch(from.branch());
reuse.setSchema(from.schema());
diff --git
a/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalType.java
b/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalType.java
index c18c8f670d..ef5a270f98 100644
---
a/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalType.java
+++
b/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalType.java
@@ -30,12 +30,17 @@ class DynamicRecordInternalType extends
TypeInformation<DynamicRecordInternal> {
private final CatalogLoader catalogLoader;
private final boolean writeSchemaAndSpec;
private final int cacheSize;
+ private final int inputSchemasPerTableCacheMaximumSize;
DynamicRecordInternalType(
- CatalogLoader catalogLoader, boolean writeSchemaAndSpec, int cacheSize) {
+ CatalogLoader catalogLoader,
+ boolean writeSchemaAndSpec,
+ int cacheSize,
+ int inputSchemasPerTableCacheMaximumSize) {
this.catalogLoader = catalogLoader;
this.writeSchemaAndSpec = writeSchemaAndSpec;
this.cacheSize = cacheSize;
+ this.inputSchemasPerTableCacheMaximumSize =
inputSchemasPerTableCacheMaximumSize;
}
@Override
@@ -71,7 +76,8 @@ class DynamicRecordInternalType extends
TypeInformation<DynamicRecordInternal> {
@Override
public TypeSerializer<DynamicRecordInternal>
createSerializer(SerializerConfig serializerConfig) {
return new DynamicRecordInternalSerializer(
- new TableSerializerCache(catalogLoader, cacheSize),
writeSchemaAndSpec);
+ new TableSerializerCache(catalogLoader, cacheSize,
inputSchemasPerTableCacheMaximumSize),
+ writeSchemaAndSpec);
}
@Override
diff --git
a/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordProcessor.java
b/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordProcessor.java
index ba2d816ae9..91f5d52b02 100644
---
a/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordProcessor.java
+++
b/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordProcessor.java
@@ -104,12 +104,14 @@ class DynamicRecordProcessor<T> extends
ProcessFunction<T, DynamicRecordInternal
this.forwardStream =
new OutputTag<>(
DYNAMIC_FORWARD_STREAM,
- new DynamicRecordInternalType(catalogLoader, true,
cacheMaximumSize)) {};
+ new DynamicRecordInternalType(
+ catalogLoader, true, cacheMaximumSize,
inputSchemasPerTableCacheMaximumSize)) {};
if (!immediateUpdate) {
updateStream =
new OutputTag<>(
DYNAMIC_TABLE_UPDATE_STREAM,
- new DynamicRecordInternalType(catalogLoader, true,
cacheMaximumSize)) {};
+ new DynamicRecordInternalType(
+ catalogLoader, true, cacheMaximumSize,
inputSchemasPerTableCacheMaximumSize)) {};
}
this.dynamicRecordWithConfig =
diff --git
a/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/TableSerializerCache.java
b/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/TableSerializerCache.java
index 13b4fa56c9..e7b37c391a 100644
---
a/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/TableSerializerCache.java
+++
b/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/TableSerializerCache.java
@@ -20,7 +20,6 @@ package org.apache.iceberg.flink.sink.dynamic;
import java.io.Serializable;
import java.util.Map;
-import javax.annotation.Nullable;
import org.apache.flink.annotation.Internal;
import org.apache.flink.annotation.VisibleForTesting;
import org.apache.flink.api.java.tuple.Tuple3;
@@ -51,43 +50,41 @@ class TableSerializerCache implements Serializable {
private final CatalogLoader catalogLoader;
private final int maximumSize;
+ private final int inputSchemasPerTableCacheMaximumSize;
// Intentionally not closed; the catalog is reused for the serializer's
lifetime.
private transient Catalog catalog;
private transient Map<String, SerializerInfo> serializers;
- TableSerializerCache(CatalogLoader catalogLoader, int maximumSize) {
+ TableSerializerCache(
+ CatalogLoader catalogLoader, int maximumSize, int
inputSchemasPerTableCacheMaximumSize) {
this.catalogLoader = catalogLoader;
this.maximumSize = maximumSize;
+ this.inputSchemasPerTableCacheMaximumSize =
inputSchemasPerTableCacheMaximumSize;
}
- RowDataSerializer serializer(String tableName, Schema schema, PartitionSpec
spec) {
- return serializer(tableName, schema, spec, null, null).f0;
+ RowDataSerializer serializer(String tableName, Schema schema) {
+ return serializerInfo(tableName).serializerBySchema(schema);
}
- Tuple3<RowDataSerializer, Schema, PartitionSpec> serializerWithSchemaAndSpec(
- String tableName, Integer schemaId, Integer specId) {
- return serializer(tableName, null, null, schemaId, specId);
+ /**
+ * Resolves the serializer when the caller already holds the schema and spec
along with their ids.
+ * The serializer is looked up by schema id, so it is not rebuilt when the
{@link Schema} instance
+ * changes.
+ *
+ * <p>The id-keyed serializers are shared with {@link
#serializerWithSchemaAndSpec}, so the schema
+ * must come from the table. Do not use this for user-supplied schemas,
whose ids are not reliable
+ * and could collide with the table's schemas.
+ */
+ RowDataSerializer serializerById(String tableName, Schema schema) {
+ return serializerInfo(tableName).serializerBySchemaId(schema);
}
- private Tuple3<RowDataSerializer, Schema, PartitionSpec> serializer(
- String tableName,
- @Nullable Schema unknownSchema,
- @Nullable PartitionSpec unknownSpec,
- @Nullable Integer schemaId,
- @Nullable Integer specId) {
- Preconditions.checkState(
- (unknownSchema == null && unknownSpec == null) ^ (schemaId == null &&
specId == null),
- "Either the full schema/spec or their ids must be provided.");
-
- if (serializers == null) {
- // We need to initialize the cache at the first time
- this.serializers = new LRUCache<>(maximumSize);
- }
-
- SerializerInfo info = serializers.computeIfAbsent(tableName,
SerializerInfo::new);
- Schema schema = unknownSchema != null ? unknownSchema :
info.schemas.get(schemaId);
- PartitionSpec spec = unknownSpec != null ? unknownSpec :
info.specs.get(specId);
+ Tuple3<RowDataSerializer, Schema, PartitionSpec> serializerWithSchemaAndSpec(
+ String tableName, Integer schemaId, Integer specId) {
+ SerializerInfo info = serializerInfo(tableName);
+ Schema schema = info.schemas.get(schemaId);
+ PartitionSpec spec = info.specs.get(specId);
if (schema == null || spec == null) {
info.update();
@@ -95,11 +92,28 @@ class TableSerializerCache implements Serializable {
spec = info.specs.get(specId);
}
- RowDataSerializer serializer =
- info.serializers.computeIfAbsent(
- schema, s -> new RowDataSerializer(FlinkSchemaUtil.convert(s)));
+ Preconditions.checkArgument(
+ schema != null,
+ "SchemaId:%s not found in table %s. Expected SchemaId:%s to exist.",
+ schemaId,
+ tableName,
+ schemaId);
+ Preconditions.checkArgument(
+ spec != null,
+ "SpecId:%s not found in table %s. Expected SpecId:%s to exist.",
+ specId,
+ tableName,
+ specId);
+
+ return Tuple3.of(info.serializerBySchemaId(schema), schema, spec);
+ }
+
+ private SerializerInfo serializerInfo(String tableName) {
+ if (serializers == null) {
+ this.serializers = new LRUCache<>(maximumSize);
+ }
- return Tuple3.of(serializer, schema, spec);
+ return serializers.computeIfAbsent(tableName, SerializerInfo::new);
}
CatalogLoader catalogLoader() {
@@ -110,19 +124,38 @@ class TableSerializerCache implements Serializable {
return maximumSize;
}
+ int inputSchemasPerTableCacheMaximumSize() {
+ return inputSchemasPerTableCacheMaximumSize;
+ }
+
private class SerializerInfo {
private final String tableName;
- private final Map<Schema, RowDataSerializer> serializers;
+ private final Map<Schema, RowDataSerializer> serializersBySchema;
+ private final Map<Integer, RowDataSerializer> serializersBySchemaId;
private Map<Integer, Schema> schemas;
private Map<Integer, PartitionSpec> specs;
SerializerInfo(String tableName) {
this.tableName = tableName;
- this.serializers = Maps.newHashMapWithExpectedSize(2);
+ this.serializersBySchema = new
LRUCache<>(inputSchemasPerTableCacheMaximumSize);
+ this.serializersBySchemaId = Maps.newHashMapWithExpectedSize(2);
this.schemas = Maps.newHashMapWithExpectedSize(1);
this.specs = Maps.newHashMapWithExpectedSize(0);
}
+ private RowDataSerializer serializerBySchema(Schema schema) {
+ return serializersBySchema.computeIfAbsent(schema,
this::createSerializer);
+ }
+
+ private RowDataSerializer serializerBySchemaId(Schema schema) {
+ return serializersBySchemaId.computeIfAbsent(
+ schema.schemaId(), id -> createSerializer(schema));
+ }
+
+ private RowDataSerializer createSerializer(Schema schema) {
+ return new RowDataSerializer(FlinkSchemaUtil.convert(schema));
+ }
+
private void update() {
if (catalog == null) {
catalog = catalogLoader.loadCatalog();
diff --git
a/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializerTestBase.java
b/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializerTestBase.java
index 07096b891a..07a2ceca99 100644
---
a/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializerTestBase.java
+++
b/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordInternalSerializerTestBase.java
@@ -65,7 +65,7 @@ abstract class DynamicRecordInternalSerializerTestBase
@Override
protected TypeSerializer<DynamicRecordInternal> createSerializer() {
return new DynamicRecordInternalSerializer(
- new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 1),
+ new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 1, 10),
writeFullSchemaAndSpec,
writeLongUTF);
}
diff --git
a/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordInternalSerializer.java
b/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordInternalSerializer.java
index 5667583599..d24076c382 100644
---
a/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordInternalSerializer.java
+++
b/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordInternalSerializer.java
@@ -20,15 +20,18 @@ package org.apache.iceberg.flink.sink.dynamic;
import static org.apache.iceberg.types.Types.NestedField.required;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
import java.io.IOException;
import java.util.Collections;
+import org.apache.flink.api.common.serialization.SerializerConfigImpl;
import org.apache.flink.api.common.typeutils.TypeSerializer;
import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
import org.apache.flink.core.memory.DataInputDeserializer;
import org.apache.flink.core.memory.DataOutputSerializer;
import org.apache.flink.table.data.GenericRowData;
import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
import org.apache.flink.types.RowKind;
import org.apache.iceberg.PartitionSpec;
import org.apache.iceberg.Schema;
@@ -107,6 +110,53 @@ public class TestDynamicRecordInternalSerializer {
.isTrue();
}
+ @Test
+ void testSerializeWithSchemaIdForMissingTable() {
+ DynamicRecordInternalSerializer serializer =
+ new DynamicRecordInternalSerializer(
+ new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 1,
10), false);
+ GenericRowData row = GenericRowData.of(123L,
StringData.fromString("test"), 1.23f);
+ DynamicRecordInternal record =
+ new DynamicRecordInternal(
+ "missingTable",
+ DynamicRecordInternalSerializerTestBase.BRANCH,
+ DynamicRecordInternalSerializerTestBase.SCHEMA,
+ row,
+ DynamicRecordInternalSerializerTestBase.SPEC,
+ 42,
+ false,
+ Collections.emptySet());
+ DataOutputSerializer output = new DataOutputSerializer(128);
+
+ assertThatCode(() -> serializer.serialize(record,
output)).doesNotThrowAnyException();
+ assertThat(output.length()).isGreaterThan(0);
+ }
+
+ @Test
+ void testCacheSizesFromType() {
+ DynamicRecordInternalType type =
+ new DynamicRecordInternalType(CATALOG_EXTENSION.catalogLoader(), true,
5, 3);
+ TypeSerializer<DynamicRecordInternal> internalTypeSerializer =
+ type.createSerializer(new SerializerConfigImpl());
+
assertThat(internalTypeSerializer).isInstanceOf(DynamicRecordInternalSerializer.class);
+ DynamicRecordInternalSerializer dynamicRecordInternalSerializer =
+ (DynamicRecordInternalSerializer) internalTypeSerializer;
+
+
assertThat(dynamicRecordInternalSerializer.getSerializerCache().maximumSize()).isEqualTo(5);
+ assertThat(
+ dynamicRecordInternalSerializer
+ .getSerializerCache()
+ .inputSchemasPerTableCacheMaximumSize())
+ .isEqualTo(3);
+
+ DynamicRecordInternalSerializer duplicate =
+ (DynamicRecordInternalSerializer)
dynamicRecordInternalSerializer.duplicate();
+ assertThat(duplicate.getSerializerCache())
+ .isNotSameAs(dynamicRecordInternalSerializer.getSerializerCache());
+ assertThat(duplicate.getSerializerCache().maximumSize()).isEqualTo(5);
+
assertThat(duplicate.getSerializerCache().inputSchemasPerTableCacheMaximumSize()).isEqualTo(3);
+ }
+
@ParameterizedTest
@ValueSource(booleans = {false, true})
void copyPreservesValues(boolean reuse) {
@@ -167,7 +217,7 @@ public class TestDynamicRecordInternalSerializer {
private DynamicRecordInternalSerializer createSerializer() {
return new DynamicRecordInternalSerializer(
- new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 1), true);
+ new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 1, 10),
true);
}
private static class OldTypeSerializerSnapshot
diff --git
a/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordProcessor.java
b/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordProcessor.java
index 48b94c14c7..6f50e62f2c 100644
---
a/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordProcessor.java
+++
b/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicRecordProcessor.java
@@ -179,7 +179,7 @@ class TestDynamicRecordProcessor {
return new OutputTag<>(
DynamicRecordProcessor.DYNAMIC_TABLE_UPDATE_STREAM,
new DynamicRecordInternalType(
- CATALOG_EXTENSION.catalogLoader(), true, CACHE_REFRESH_MS)) {};
+ CATALOG_EXTENSION.catalogLoader(), true, CACHE_REFRESH_MS, 10)) {};
}
private static DynamicRecord recordWithDistributionMode(
diff --git
a/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestTableSerializerCache.java
b/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestTableSerializerCache.java
index bbfb39618f..156ec0def0 100644
---
a/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestTableSerializerCache.java
+++
b/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestTableSerializerCache.java
@@ -24,6 +24,7 @@ import static
org.apache.iceberg.types.Types.NestedField.optional;
import static org.apache.iceberg.types.Types.NestedField.required;
import static org.apache.iceberg.types.Types.StringType;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.function.Supplier;
import org.apache.flink.api.java.tuple.Tuple3;
@@ -52,16 +53,13 @@ class TestTableSerializerCache {
optional(2, "data", StringType.get()),
optional(3, "double", DoubleType.get()));
- TableSerializerCache cache = new
TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 10);
+ TableSerializerCache cache = new
TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 10, 10);
@Test
void testFullSchemaCaching() {
- Supplier<RowDataSerializer> creator1a =
- () -> cache.serializer("table", schema1,
PartitionSpec.unpartitioned());
- Supplier<RowDataSerializer> creator1b =
- () -> cache.serializer("table", schema2,
PartitionSpec.unpartitioned());
- Supplier<RowDataSerializer> creator2 =
- () -> cache.serializer("table2", schema2,
PartitionSpec.unpartitioned());
+ Supplier<RowDataSerializer> creator1a = () -> cache.serializer("table",
schema1);
+ Supplier<RowDataSerializer> creator1b = () -> cache.serializer("table",
schema2);
+ Supplier<RowDataSerializer> creator2 = () -> cache.serializer("table2",
schema2);
RowDataSerializer serializer1a = creator1a.get();
RowDataSerializer serializer1b = creator1b.get();
@@ -73,10 +71,32 @@ class TestTableSerializerCache {
assertThat(serializer2).isSameAs(creator2.get());
}
+ @Test
+ void testSerializerById() {
+ LoadCountingCatalogLoader catalogLoader =
+ new LoadCountingCatalogLoader(CATALOG_EXTENSION.catalogLoader());
+ cache = new TableSerializerCache(catalogLoader, 10, 10);
+
+ Schema schema1Copy = new Schema(schema1.schemaId(), required(1, "id",
LongType.get()));
+ Supplier<RowDataSerializer> creator1a = () ->
cache.serializerById("table", schema1);
+ Supplier<RowDataSerializer> creator1b = () ->
cache.serializerById("table", schema1Copy);
+ Supplier<RowDataSerializer> creator2 = () -> cache.serializerById("table",
schema2);
+ Supplier<RowDataSerializer> creator3 = () ->
cache.serializerById("table2", schema1);
+
+ RowDataSerializer serializer1a = creator1a.get();
+ RowDataSerializer serializer1b = creator1b.get();
+ RowDataSerializer serializer2 = creator2.get();
+ RowDataSerializer serializer3 = creator3.get();
+
+ assertThat(serializer1a).isNotSameAs(serializer2).isNotSameAs(serializer3);
+ assertThat(serializer1b).isSameAs(serializer1a);
+ assertThat(catalogLoader.loadCount()).isEqualTo(0);
+ }
+
@Test
void testCachingWithSchemaLookup() {
CatalogLoader catalogLoader = CATALOG_EXTENSION.catalogLoader();
- cache = new TableSerializerCache(catalogLoader, 10);
+ cache = new TableSerializerCache(catalogLoader, 10, 10);
Catalog catalog = catalogLoader.loadCatalog();
Table table = catalog.createTable(TableIdentifier.of("table"), schema1);
@@ -98,15 +118,87 @@ class TestTableSerializerCache {
assertThat(serializerWithSchemaAndSpec.f2).isSameAs(serializerWithSchemaAndSpec2.f2);
}
+ @Test
+ void testSchemaLookupWithSchemaEvolution() {
+ Table table =
CATALOG_EXTENSION.catalog().createTable(TableIdentifier.of("table"), schema1);
+ cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 10,
10);
+ int specId = table.spec().specId();
+ int schemaId1 = table.schema().schemaId();
+ Supplier<RowDataSerializer> creator1a =
+ () -> cache.serializerWithSchemaAndSpec("table", schemaId1, specId).f0;
+
+ table.updateSchema().addColumn("data", StringType.get()).commit();
+
+ int schemaId2 = table.schema().schemaId();
+ Supplier<RowDataSerializer> creator2 =
+ () -> cache.serializerWithSchemaAndSpec("table", schemaId2, specId).f0;
+ Supplier<RowDataSerializer> creator1b =
+ () -> cache.serializerWithSchemaAndSpec("table", schemaId1, specId).f0;
+
+ assertThat(creator2.get()).isNotSameAs(creator1a.get());
+ assertThat(creator1b.get()).isSameAs(creator1a.get());
+ }
+
+ @Test
+ void testSchemaSerializerEviction() {
+ cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 10, 1);
+ Supplier<RowDataSerializer> creator1 = () -> cache.serializer("table",
schema1);
+ Supplier<RowDataSerializer> creator2 = () -> cache.serializer("table",
schema2);
+
+ RowDataSerializer serializer1 = creator1.get();
+ assertThat(creator1.get()).isSameAs(serializer1);
+
+ // Only one schema fits, so caching schema2 evicts the serializer for
schema1
+ RowDataSerializer serializer2 = creator2.get();
+ assertThat(creator2.get()).isSameAs(serializer2);
+ assertThat(creator1.get()).isNotSameAs(serializer1);
+ }
+
+ @Test
+ void testSerializerByIdSharedWithSchemaLookup() {
+ Table table =
CATALOG_EXTENSION.catalog().createTable(TableIdentifier.of("table"), schema1);
+ cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 10,
10);
+ int schemaId = table.schema().schemaId();
+ int specId = table.spec().specId();
+ Supplier<RowDataSerializer> creator1 = () -> cache.serializerById("table",
table.schema());
+ Supplier<RowDataSerializer> creator2 =
+ () -> cache.serializerWithSchemaAndSpec("table", schemaId, specId).f0;
+
+ RowDataSerializer serializer1 = creator1.get();
+
+ assertThat(creator2.get()).isSameAs(serializer1);
+ assertThat(creator1.get()).isSameAs(serializer1);
+ }
+
+ @Test
+ void testSchemaLookupWithUnknownSchemaId() {
+ Table table =
CATALOG_EXTENSION.catalog().createTable(TableIdentifier.of("table"), schema1);
+ cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 10,
10);
+ int specId = table.spec().specId();
+
+ assertThatThrownBy(() -> cache.serializerWithSchemaAndSpec("table", 999,
specId))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("SchemaId:999 not found in table table. Expected
SchemaId:999 to exist.");
+ }
+
+ @Test
+ void testSchemaLookupWithUnknownSpecId() {
+ Table table =
CATALOG_EXTENSION.catalog().createTable(TableIdentifier.of("table"), schema1);
+ cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 10,
10);
+ int schemaId = table.schema().schemaId();
+
+ assertThatThrownBy(() -> cache.serializerWithSchemaAndSpec("table",
schemaId, 999))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("SpecId:999 not found in table table. Expected SpecId:999
to exist.");
+ }
+
@Test
void testCacheEviction() {
- cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 0);
+ cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 0, 10);
assertThat(cache.maximumSize()).isEqualTo(0);
- Supplier<RowDataSerializer> creator1 =
- () -> cache.serializer("table", schema1,
PartitionSpec.unpartitioned());
- Supplier<RowDataSerializer> creator2 =
- () -> cache.serializer("table2", schema2,
PartitionSpec.unpartitioned());
+ Supplier<RowDataSerializer> creator1 = () -> cache.serializer("table",
schema1);
+ Supplier<RowDataSerializer> creator2 = () -> cache.serializer("table2",
schema2);
RowDataSerializer serializer1 = creator1.get();
RowDataSerializer serializer2 = creator2.get();
@@ -118,7 +210,7 @@ class TestTableSerializerCache {
@Test
void testCacheSize() {
- cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 1000);
+ cache = new TableSerializerCache(CATALOG_EXTENSION.catalogLoader(), 1000,
10);
assertThat(cache.maximumSize()).isEqualTo(1000);
}
@@ -129,7 +221,7 @@ class TestTableSerializerCache {
LoadCountingCatalogLoader catalogLoader =
new LoadCountingCatalogLoader(CATALOG_EXTENSION.catalogLoader());
- cache = new TableSerializerCache(catalogLoader, 10);
+ cache = new TableSerializerCache(catalogLoader, 10, 10);
// schema/spec ids are unknown, so both lookups miss the cache and need
the catalog
cache.serializerWithSchemaAndSpec(