Skip to content

Commit 89c508b

Browse files
committed
review(common): fold Avro schema cache into HoodieSchema.fromAvroSchema, drop AvroToHoodieSchemaCache
1 parent 31824fe commit 89c508b

9 files changed

Lines changed: 36 additions & 77 deletions

File tree

hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/table/format/FlinkRecordContext.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import org.apache.hudi.common.model.HoodieKey;
2626
import org.apache.hudi.common.model.HoodieOperation;
2727
import org.apache.hudi.common.model.HoodieRecord;
28-
import org.apache.hudi.common.schema.AvroToHoodieSchemaCache;
2928
import org.apache.hudi.common.schema.HoodieSchema;
3029
import org.apache.hudi.common.schema.HoodieSchemaField;
3130
import org.apache.hudi.common.table.HoodieTableConfig;
@@ -126,7 +125,7 @@ public RowData getDeleteRow(String recordKey) {
126125
@Override
127126
public RowData convertAvroRecord(IndexedRecord avroRecord) {
128127
Schema recordSchema = avroRecord.getSchema();
129-
AvroToRowDataConverters.AvroToRowDataConverter converter = RowDataQueryContexts.fromSchema(AvroToHoodieSchemaCache.intern(recordSchema), utcTimezone).getAvroToRowDataConverter();
128+
AvroToRowDataConverters.AvroToRowDataConverter converter = RowDataQueryContexts.fromSchema(HoodieSchema.fromAvroSchema(recordSchema), utcTimezone).getAvroToRowDataConverter();
130129
RowData rowData = (RowData) converter.convert(avroRecord);
131130
Schema.Field operationField = recordSchema.getField(HoodieRecord.OPERATION_METADATA_FIELD);
132131
if (operationField != null) {

hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/SparkFileFormatInternalRecordContext.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ package org.apache.hudi
2121

2222
import org.apache.avro.generic.{GenericRecord, IndexedRecord}
2323
import org.apache.hudi.common.engine.RecordContext
24-
import org.apache.hudi.common.schema.{AvroToHoodieSchemaCache, HoodieSchema}
24+
import org.apache.hudi.common.schema.HoodieSchema
2525
import org.apache.hudi.common.table.HoodieTableConfig
2626
import org.apache.spark.sql.HoodieInternalRowUtils
2727
import org.apache.spark.sql.avro.{HoodieAvroDeserializer, HoodieAvroSerializer}
@@ -47,7 +47,7 @@ trait SparkFileFormatInternalRecordContext extends BaseSparkInternalRecordContex
4747
* @return An [[InternalRow]].
4848
*/
4949
override def convertAvroRecord(avroRecord: IndexedRecord): InternalRow = {
50-
val schema = AvroToHoodieSchemaCache.intern(avroRecord.getSchema)
50+
val schema = HoodieSchema.fromAvroSchema(avroRecord.getSchema)
5151
val structType = HoodieInternalRowUtils.getCachedSchema(schema)
5252
val deserializer = deserializerMap.getOrElseUpdate(schema, {
5353
sparkAdapter.createAvroDeserializer(schema, structType)

hudi-common/src/main/java/org/apache/hudi/avro/AvroRecordContext.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import org.apache.hudi.common.model.HoodieEmptyRecord;
2525
import org.apache.hudi.common.model.HoodieKey;
2626
import org.apache.hudi.common.model.HoodieRecord;
27-
import org.apache.hudi.common.schema.AvroToHoodieSchemaCache;
2827
import org.apache.hudi.common.schema.HoodieSchema;
2928
import org.apache.hudi.common.schema.HoodieSchemaField;
3029
import org.apache.hudi.common.table.HoodieTableConfig;
@@ -71,10 +70,7 @@ public AvroRecordContext() {
7170
public static Object getFieldValueFromIndexedRecord(
7271
IndexedRecord record,
7372
String fieldName) {
74-
// Interning returns the canonical wrapper for this schema, whose lazily built field list and
75-
// field map survive across calls, so the per-record cost is a cache hit instead of an
76-
// O(schema width) wrapper rebuild.
77-
HoodieSchema currentSchema = AvroToHoodieSchemaCache.intern(record.getSchema());
73+
HoodieSchema currentSchema = HoodieSchema.fromAvroSchema(record.getSchema());
7874
IndexedRecord currentRecord = record;
7975
String[] path = fieldName.split("\\.");
8076
for (int i = 0; i < path.length; i++) {

hudi-common/src/main/java/org/apache/hudi/avro/HoodieAvroUtils.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
package org.apache.hudi.avro;
2020

2121
import org.apache.hudi.common.model.HoodieRecord;
22-
import org.apache.hudi.common.schema.AvroToHoodieSchemaCache;
2322
import org.apache.hudi.common.schema.HoodieSchema;
2423
import org.apache.hudi.common.schema.HoodieSchemaUtils;
2524
import org.apache.hudi.common.util.DateTimeUtils;
@@ -835,7 +834,7 @@ public static Object[] getRecordColumnValues(HoodieRecord record,
835834
Schema schema,
836835
boolean consistentLogicalTimestampEnabled) {
837836
try {
838-
GenericRecord genericRecord = (GenericRecord) (record.toIndexedRecord(AvroToHoodieSchemaCache.intern(schema), new Properties()).get()).getData();
837+
GenericRecord genericRecord = (GenericRecord) (record.toIndexedRecord(HoodieSchema.fromAvroSchema(schema), new Properties()).get()).getData();
839838
List<Object> list = new ArrayList<>();
840839
for (String col : columns) {
841840
list.add(HoodieAvroUtils.getNestedFieldVal(genericRecord, col, true, consistentLogicalTimestampEnabled));

hudi-common/src/main/java/org/apache/hudi/common/HoodieJsonPayload.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
import org.apache.hudi.avro.MercifulJsonConverter;
2222
import org.apache.hudi.common.model.HoodieRecordPayload;
23-
import org.apache.hudi.common.schema.AvroToHoodieSchemaCache;
23+
import org.apache.hudi.common.schema.HoodieSchema;
2424
import org.apache.hudi.common.util.Option;
2525
import org.apache.hudi.exception.HoodieException;
2626
import org.apache.hudi.io.util.FileIOUtils;
@@ -65,7 +65,7 @@ public Option<IndexedRecord> combineAndGetUpdateValue(IndexedRecord oldRec, Sche
6565
@Override
6666
public Option<IndexedRecord> getInsertValue(Schema schema) throws IOException {
6767
MercifulJsonConverter jsonConverter = new MercifulJsonConverter();
68-
return Option.of(jsonConverter.convert(getJsonData(), AvroToHoodieSchemaCache.intern(schema)));
68+
return Option.of(jsonConverter.convert(getJsonData(), HoodieSchema.fromAvroSchema(schema)));
6969
}
7070

7171
private String getJsonData() throws IOException {

hudi-common/src/main/java/org/apache/hudi/common/schema/AvroToHoodieSchemaCache.java

Lines changed: 0 additions & 52 deletions
This file was deleted.

hudi-common/src/main/java/org/apache/hudi/common/schema/HoodieSchema.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import org.apache.hudi.exception.HoodieIOException;
2727
import org.apache.hudi.internal.schema.HoodieSchemaException;
2828

29+
import com.github.benmanes.caffeine.cache.Caffeine;
30+
import com.github.benmanes.caffeine.cache.LoadingCache;
2931
import lombok.Getter;
3032
import org.apache.avro.JsonProperties;
3133
import org.apache.avro.LogicalType;
@@ -350,16 +352,32 @@ private HoodieSchema(Schema avroSchema, List<HoodieSchemaField> fields) {
350352
this.fields = fields != null ? Collections.unmodifiableList(fields) : null;
351353
}
352354

355+
// Avro schemas are interned by identity (records of one file share the same Schema instance), so the
356+
// per-record fromAvroSchema() call is a cache hit that reuses the canonical HoodieSchema and its lazily
357+
// built field list / field map instead of rebuilding an O(schema width) wrapper. Misses convert and
358+
// value-intern through HoodieSchemaCache so equal-but-distinct Avro schema instances still converge on
359+
// one canonical HoodieSchema. Global cache for the JVM lifecycle; weakKeys lets dead schemas be GC'd.
360+
private static final LoadingCache<Schema, HoodieSchema> AVRO_SCHEMA_CACHE =
361+
Caffeine.newBuilder().weakKeys().maximumSize(1024)
362+
.build(avroSchema -> HoodieSchemaCache.intern(convertFromAvroSchema(avroSchema)));
363+
353364
/**
354-
* Factory method to create HoodieSchema from an Avro schema.
365+
* Factory method to create a {@link HoodieSchema} from an Avro schema.
366+
*
367+
* <p>The result is interned: passing the same Avro {@link Schema} instance (e.g. once per record)
368+
* returns the canonical {@link HoodieSchema} rather than rebuilding a fresh wrapper each call.
355369
*
356370
* @param avroSchema the Avro schema to wrap
357-
* @return new HoodieSchema instance
371+
* @return canonical HoodieSchema instance, or {@code null} if {@code avroSchema} is null
358372
*/
359373
public static HoodieSchema fromAvroSchema(Schema avroSchema) {
360374
if (avroSchema == null) {
361375
return null;
362376
}
377+
return AVRO_SCHEMA_CACHE.get(avroSchema);
378+
}
379+
380+
private static HoodieSchema convertFromAvroSchema(Schema avroSchema) {
363381
LogicalType logicalType = avroSchema.getLogicalType();
364382
if (logicalType != null) {
365383
if (logicalType instanceof LogicalTypes.Decimal) {

hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/realtime/RealtimeCompactedRecordReader.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.apache.hudi.common.model.HoodieAvroRecordMerger;
2828
import org.apache.hudi.common.model.HoodieRecord;
2929
import org.apache.hudi.common.model.HoodieRecordMerger;
30-
import org.apache.hudi.common.schema.AvroToHoodieSchemaCache;
3130
import org.apache.hudi.common.schema.HoodieSchema;
3231
import org.apache.hudi.common.table.log.HoodieMergedLogRecordScanner;
3332
import org.apache.hudi.common.table.read.BufferedRecord;
@@ -205,8 +204,8 @@ private Option<HoodieAvroIndexedRecord> mergeRecord(HoodieRecord<?> newRecord, A
205204
// once presto on hudi have its own mor reader, we can remove the rewrite logical.
206205
GenericRecord genericRecord = HiveAvroSerializer.rewriteRecordIgnoreResultCheck(oldRecord, getLogScannerReaderSchema());
207206
RecordContext<IndexedRecord> recordContext = AvroRecordContext.getFieldAccessorInstance();
208-
BufferedRecord record = BufferedRecords.fromEngineRecord(genericRecord, AvroToHoodieSchemaCache.intern(genericRecord.getSchema()), recordContext, orderingFields, newRecord.getRecordKey(), false);
209-
BufferedRecord newBufferedRecord = BufferedRecords.fromHoodieRecord(newRecord, AvroToHoodieSchemaCache.intern(getLogScannerReaderSchema().toAvroSchema()),
207+
BufferedRecord record = BufferedRecords.fromEngineRecord(genericRecord, HoodieSchema.fromAvroSchema(genericRecord.getSchema()), recordContext, orderingFields, newRecord.getRecordKey(), false);
208+
BufferedRecord newBufferedRecord = BufferedRecords.fromHoodieRecord(newRecord, HoodieSchema.fromAvroSchema(getLogScannerReaderSchema().toAvroSchema()),
210209
recordContext, payloadProps, orderingFields, deleteContext);
211210
BufferedRecord mergeResult = merger.merge(record, newBufferedRecord, recordContext, payloadProps);
212211
if (mergeResult.isDelete()) {

hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/command/payload/ExpressionPayload.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import org.apache.hudi.HoodieSchemaConversionUtils.{convertHoodieSchemaToDataTyp
2222
import org.apache.hudi.SparkAdapterSupport.sparkAdapter
2323
import org.apache.hudi.avro.HoodieAvroUtils
2424
import org.apache.hudi.common.model.{DefaultHoodieRecordPayload, HoodiePayloadProps, HoodieRecord, HoodieRecordPayload, OverwriteWithLatestAvroPayload}
25-
import org.apache.hudi.common.schema.{AvroToHoodieSchemaCache, HoodieSchema, HoodieSchemaUtils}
25+
import org.apache.hudi.common.schema.{HoodieSchema, HoodieSchemaUtils}
2626
import org.apache.hudi.common.util.{BinaryUtil, ConfigUtils, HoodieRecordUtils, Option => HOption, OrderingValues, StringUtils, ValidationUtils}
2727
import org.apache.hudi.common.util.ValidationUtils.checkState
2828
import org.apache.hudi.config.HoodieWriteConfig
@@ -116,7 +116,7 @@ class ExpressionPayload(@transient record: GenericRecord,
116116

117117
// Get the Evaluator for each condition and update assignments.
118118
val updateConditionAndAssignments =
119-
getEvaluator(updateConditionAndAssignmentsText.toString, AvroToHoodieSchemaCache.intern(inputRecord.asAvro.getSchema))
119+
getEvaluator(updateConditionAndAssignmentsText.toString, HoodieSchema.fromAvroSchema(inputRecord.asAvro.getSchema))
120120

121121
for ((conditionEvaluator, assignmentEvaluator) <- updateConditionAndAssignments
122122
if resultRecordOpt == null) {
@@ -145,7 +145,7 @@ class ExpressionPayload(@transient record: GenericRecord,
145145
// Process delete
146146
val deleteConditionText = properties.get(ExpressionPayload.PAYLOAD_DELETE_CONDITION)
147147
if (deleteConditionText != null) {
148-
val (deleteConditionEvaluator, _) = getEvaluator(deleteConditionText.toString, AvroToHoodieSchemaCache.intern(inputRecord.asAvro.getSchema)).head
148+
val (deleteConditionEvaluator, _) = getEvaluator(deleteConditionText.toString, HoodieSchema.fromAvroSchema(inputRecord.asAvro.getSchema)).head
149149
val deleteConditionEvalResult = deleteConditionEvaluator.apply(inputRecord.asRow)
150150
.get(0, BooleanType)
151151
.asInstanceOf[Boolean]
@@ -206,7 +206,7 @@ class ExpressionPayload(@transient record: GenericRecord,
206206
* multiple times for different expression evaluation invocations
207207
*/
208208
case class ConvertibleRecord(private val avro: GenericRecord) extends Logging {
209-
private lazy val row: InternalRow = getAvroDeserializerFor(AvroToHoodieSchemaCache.intern(avro.getSchema)).deserialize(avro) match {
209+
private lazy val row: InternalRow = getAvroDeserializerFor(HoodieSchema.fromAvroSchema(avro.getSchema)).deserialize(avro) match {
210210
case Some(row) => row.asInstanceOf[InternalRow]
211211
case None =>
212212
logError(s"Failed to deserialize Avro record `${avro.toString}` as Catalyst row")
@@ -231,7 +231,7 @@ class ExpressionPayload(@transient record: GenericRecord,
231231
properties.get(ExpressionPayload.PAYLOAD_INSERT_CONDITION_AND_ASSIGNMENTS).toString
232232
// Get the evaluator for each condition and insert assignment.
233233
val insertConditionAndAssignments =
234-
ExpressionPayload.getEvaluator(insertConditionAndAssignmentsText, AvroToHoodieSchemaCache.intern(inputRecord.asAvro.getSchema))
234+
ExpressionPayload.getEvaluator(insertConditionAndAssignmentsText, HoodieSchema.fromAvroSchema(inputRecord.asAvro.getSchema))
235235
var resultRecordOpt: HOption[IndexedRecord] = null
236236
for ((conditionEvaluator, assignmentEvaluator) <- insertConditionAndAssignments
237237
if resultRecordOpt == null) {
@@ -243,7 +243,7 @@ class ExpressionPayload(@transient record: GenericRecord,
243243
if (conditionEvalResult) {
244244
val writerSchema = getWriterSchema(properties, false)
245245
val resultingRow = assignmentEvaluator.apply(inputRecord.asRow)
246-
val resultingAvroRecord = getAvroSerializerFor(AvroToHoodieSchemaCache.intern(writerSchema.getAvroSchema))
246+
val resultingAvroRecord = getAvroSerializerFor(HoodieSchema.fromAvroSchema(writerSchema.getAvroSchema))
247247
.serialize(resultingRow)
248248
.asInstanceOf[GenericRecord]
249249

@@ -315,7 +315,7 @@ class ExpressionPayload(@transient record: GenericRecord,
315315
*/
316316
private def joinRecord(sourceRecord: IndexedRecord, targetRecord: IndexedRecord, props: Properties): GenericRecord = {
317317
val leftSchema = sourceRecord.getSchema
318-
val joinSchema = getMergedSchema(AvroToHoodieSchemaCache.intern(leftSchema), AvroToHoodieSchemaCache.intern(targetRecord.getSchema))
318+
val joinSchema = getMergedSchema(HoodieSchema.fromAvroSchema(leftSchema), HoodieSchema.fromAvroSchema(targetRecord.getSchema))
319319

320320
// TODO rebase onto JoinRecord
321321
val values = new Array[AnyRef](joinSchema.getFields.size())

0 commit comments

Comments
 (0)