Guruguha Marur Sreenivasa
01/19/2023, 2:09 AMConfluentRegistryAvroDeserializationSchema.forGeneric(...) method in the flink-avro-confluent-registry package.
1. What is the use of passing this as an argument? The way I understood it, its only used to set the writer schema and nothing else.
2. If say I have a topic with 2 records r1 with schema s1 and r2 with schema s2 (s1 and s2 are slightly different - s2 has one new column than s1). Both schemas are registered in the schema registry. When I run a FlinkKafkaConsumer, and configure the deserialization schema as ConfluentRegistryAvroDeserializationSchema.forGeneric(s1, URL), I see that the new column in r2 is completely ignored. I was under the impression that if the latest schema s2 is compatible with the record that was read, and that it would be used to deserialize the record. But now, that is not happening. What is the point of passing the URL?
How do I make my consumer in such a way that they don't need to know the schema beforehand, but use the writer schema from the registry?
Additionally, I tried to follow this post to write my own deserialization logic, but it fails with a kryo exception. 😞
Can someone please elaborate on this?sap1ens
01/19/2023, 3:26 AMHow do I make my consumer in such a way that they don’t need to know the schema beforehand, but use the writer schema from the registry?• First, you get the relevant schema from the registry using
io.confluent.kafka.schemaregistry.client.SchemaRegistryClient, e.g. schemaRegistryClient.getLatestSchemaMetadata(schemaSubject).getSchema
• Then you convert it to a DataType using AvroSchemaConverter, e.g. val dataType: DataType = AvroSchemaConverter.convertToDataType(avroSchemaString)
• Cast DataType to RowType: val rowType = dataType.getLogicalType.asInstanceOf[RowType]
• Then use a custom KafkaRecordDeserializationSchema and create a converter using AvroToRowDataConverters.createRowConverter(rowType)
• Then you can convert raw ConsumerRecord[Array[Byte], Array[Byte]] to RowData like this val rowData = converter.convert(avroPayload).asInstanceOf[RowData]sap1ens
01/19/2023, 3:26 AMsap1ens
01/19/2023, 3:27 AMsap1ens
01/19/2023, 3:28 AMRegistryAvroDeserializationSchema[GenericRecord](classOf[GenericRecord], schema, schemaCoderProvider)
You use it first to get the GenericRecord from the raw ConsumerRecord: payloadDeserializer.deserialize(record.value())Guruguha Marur Sreenivasa
01/19/2023, 3:50 AMFlinkKafkaConsumer / KafkaSource client should look like this:
FlinkKafkaConsumer<GenericRecord> consumer = new FlinkKafkaConsumer<>(
properties.getProperty("source.topic"),
ConfluentRegistryAvroDeserializationSchema.forGeneric("REGISTRY_URL"),
kafkaProperties);
consumer.setStartFromEarliest();
DataStream<GenericRecord> consumerStream = env.addSource(consumer);sap1ens
01/19/2023, 3:52 AMGuruguha Marur Sreenivasa
01/19/2023, 4:58 PMenv.getConfig().enableForceAvro();
I am ok with GenericRecord as the avro schema will usually have 1000s of columns and using a POJO or SpecificRecord format would be very cumbersome.
My custom deserialization schema looks like this:
public class KafkaGenericAvroDeserializationSchema implements KafkaDeserializationSchema<GenericRecord> {
private final String registryUrl;
private transient KafkaAvroDeserializer avroDeserializer;
public KafkaGenericAvroDeserializationSchema(String registryUrl) {
this.registryUrl = registryUrl;
}
@Override
public void open(DeserializationSchema.InitializationContext context) throws Exception {
checkInitialized();
KafkaDeserializationSchema.super.open(context);
}
@Override
public boolean isEndOfStream(GenericRecord genericRecord) {
return false;
}
@Override
public GenericRecord deserialize(ConsumerRecord<byte[], byte[]> consumerRecord) throws Exception {
return (GenericRecord) avroDeserializer.deserialize("sod-sample-confluent", consumerRecord.value());
}
@Override
public void deserialize(ConsumerRecord<byte[], byte[]> consumerRecord, Collector<GenericRecord> collector)
throws IOException {
collector.collect((GenericRecord) avroDeserializer.deserialize("avro-sample-confluent", consumerRecord.value()));
}
private void checkInitialized() {
if (avroDeserializer == null) {
Map<String, Object> props = new HashMap<>();
props.put(AbstractKafkaAvroSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, registryUrl);
props.put(KafkaAvroDeserializerConfig.SPECIFIC_AVRO_READER_CONFIG, false);
SchemaRegistryClient client =
new CachedSchemaRegistryClient(
registryUrl, AbstractKafkaAvroSerDeConfig.MAX_SCHEMAS_PER_SUBJECT_DEFAULT);
avroDeserializer = new KafkaAvroDeserializer(client, props);
}
}
@Override
public TypeInformation<GenericRecord> getProducedType() {
return TypeExtractor.getForClass(GenericRecord.class);
}
}
This is leading to the following exception:
Caused by: com.esotericsoftware.kryo.KryoException: java.lang.UnsupportedOperationException
Serialization trace:
reserved (org.apache.avro.Schema$Field)
fieldMap (org.apache.avro.Schema$RecordSchema)
schema (org.apache.avro.generic.GenericData$Record)
at com.esotericsoftware.kryo.serializers.ObjectField.read(ObjectField.java:125)
at com.esotericsoftware.kryo.serializers.FieldSerializer.read(FieldSerializer.java:528)
at com.esotericsoftware.kryo.Kryo.readClassAndObject(Kryo.java:761)
at com.esotericsoftware.kryo.serializers.MapSerializer.read(MapSerializer.java:143)
at com.esotericsoftware.kryo.serializers.MapSerializer.read(MapSerializer.java:21)
at com.esotericsoftware.kryo.Kryo.readObject(Kryo.java:679)
at com.esotericsoftware.kryo.serializers.ObjectField.read(ObjectField.java:106)
at com.esotericsoftware.kryo.serializers.FieldSerializer.read(FieldSerializer.java:528)
at com.esotericsoftware.kryo.Kryo.readObject(Kryo.java:679)
at com.esotericsoftware.kryo.serializers.ObjectField.read(ObjectField.java:106)
at com.esotericsoftware.kryo.serializers.FieldSerializer.read(FieldSerializer.java:528)
at com.esotericsoftware.kryo.Kryo.readClassAndObject(Kryo.java:761)
at org.apache.flink.api.java.typeutils.runtime.kryo.KryoSerializer.deserialize(KryoSerializer.java:354)
at org.apache.flink.streaming.runtime.streamrecord.StreamElementSerializer.deserialize(StreamElementSerializer.java:191)
at org.apache.flink.streaming.runtime.streamrecord.StreamElementSerializer.deserialize(StreamElementSerializer.java:46)
at org.apache.flink.runtime.plugable.NonReusingDeserializationDelegate.read(NonReusingDeserializationDelegate.java:53)
at org.apache.flink.runtime.io.network.api.serialization.NonSpanningWrapper.readInto(NonSpanningWrapper.java:337)
at org.apache.flink.runtime.io.network.api.serialization.SpillingAdaptiveSpanningRecordDeserializer.readNonSpanningRecord(SpillingAdaptiveSpanningRecordDeserializer.java:128)
at org.apache.flink.runtime.io.network.api.serialization.SpillingAdaptiveSpanningRecordDeserializer.readNextRecord(SpillingAdaptiveSpanningRecordDeserializer.java:103)
at org.apache.flink.runtime.io.network.api.serialization.SpillingAdaptiveSpanningRecordDeserializer.getNextRecord(SpillingAdaptiveSpanningRecordDeserializer.java:93)
at org.apache.flink.streaming.runtime.io.AbstractStreamTaskNetworkInput.emitNext(AbstractStreamTaskNetworkInput.java:95)
at org.apache.flink.streaming.runtime.io.StreamOneInputProcessor.processInput(StreamOneInputProcessor.java:65)
at org.apache.flink.streaming.runtime.tasks.StreamTask.processInput(StreamTask.java:496)
at org.apache.flink.streaming.runtime.tasks.mailbox.MailboxProcessor.runMailboxLoop(MailboxProcessor.java:203)
at org.apache.flink.streaming.runtime.tasks.StreamTask.runMailboxLoop(StreamTask.java:809)
at org.apache.flink.streaming.runtime.tasks.StreamTask.invoke(StreamTask.java:761)
at org.apache.flink.runtime.taskmanager.Task.runWithSystemExitMonitoring(Task.java:958)
at org.apache.flink.runtime.taskmanager.Task.restoreAndInvoke(Task.java:937)
at org.apache.flink.runtime.taskmanager.Task.doRun(Task.java:766)
at org.apache.flink.runtime.taskmanager.Task.run(Task.java:575)
at java.lang.Thread.run(Thread.java:750)
Caused by: java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableCollection.add(Collections.java:1057)
at com.esotericsoftware.kryo.serializers.CollectionSerializer.read(CollectionSerializer.java:109)
at com.esotericsoftware.kryo.serializers.CollectionSerializer.read(CollectionSerializer.java:22)
at com.esotericsoftware.kryo.Kryo.readObject(Kryo.java:679)
at com.esotericsoftware.kryo.serializers.ObjectField.read(ObjectField.java:106)
Any idea why this happens?Guruguha Marur Sreenivasa
01/19/2023, 4:58 PMsap1ens
01/19/2023, 5:22 PMenv.getConfig().enableForceAvro(); work.
Instead, I make sure that there are no steps in my topology that require serialization and I convert GenericRecord to a class as soon as possible.Guruguha Marur Sreenivasa
01/19/2023, 7:18 PMGuruguha Marur Sreenivasa
01/19/2023, 9:43 PMsap1ens
01/19/2023, 9:45 PMReme Ajayi
01/21/2023, 6:51 PM<GenericRecord>, forGeneric with your FlinkKafkaConsumer ?Reme Ajayi
01/21/2023, 7:21 PMConfluentRegistryAvroDeserializationSchema.forGeneric ?Reme Ajayi
01/21/2023, 7:26 PMGuruguha Marur Sreenivasa
01/21/2023, 7:48 PMforGeneric(...) method.
If not, you can do what @sap1enssuggested. Below is the code snippet for the custom deserialization schema I had to implement:
import io.confluent.kafka.schemaregistry.client.CachedSchemaRegistryClient;
import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException;
import io.confluent.kafka.serializers.AbstractKafkaAvroSerDeConfig;
import io.confluent.kafka.serializers.KafkaAvroDeserializer;
import io.confluent.kafka.serializers.KafkaAvroDeserializerConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericRecord;
import org.apache.flink.api.common.serialization.DeserializationSchema;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.typeutils.TypeExtractor;
import org.apache.flink.streaming.connectors.kafka.KafkaDeserializationSchema;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class KafkaGenericAvroDeserializationSchema implements KafkaDeserializationSchema<GenericRecord> {
private final String registryUrl;
private final String sourceTopic;
private transient KafkaAvroDeserializer avroDeserializer;
private static final Map<Integer, Schema> SCHEMA_CACHE = new HashMap<>();
private CachedSchemaRegistryClient cachedSchemaRegistryClient;
public KafkaGenericAvroDeserializationSchema(String registryUrl, String sourceTopic) {
this.registryUrl = registryUrl;
this.sourceTopic = sourceTopic;
}
@Override
public void open(DeserializationSchema.InitializationContext context) throws Exception {
cachedSchemaRegistryClient = new CachedSchemaRegistryClient(
registryUrl, AbstractKafkaAvroSerDeConfig.MAX_SCHEMAS_PER_SUBJECT_DEFAULT);
KafkaDeserializationSchema.super.open(context);
}
@Override
public boolean isEndOfStream(GenericRecord genericRecord) {
return false;
}
@Override
public GenericRecord deserialize(ConsumerRecord<byte[], byte[]> consumerRecord) throws Exception {
long start = System.currentTimeMillis();
final Schema schema = checkInitialized(consumerRecord.value());
GenericRecord record = (GenericRecord) avroDeserializer.deserialize("sod-sample-confluent", consumerRecord.value(),
schema);
long end = System.currentTimeMillis();
<http://log.info|log.info>("deserialize time taken: {}ms", end-start);
return record;
}
private Schema checkInitialized(byte[] value) throws RestClientException, IOException {
if (avroDeserializer == null) {
Map<String, Object> props = new HashMap<>();
props.put(AbstractKafkaAvroSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, registryUrl);
props.put(KafkaAvroDeserializerConfig.SPECIFIC_AVRO_READER_CONFIG, false);
avroDeserializer = new KafkaAvroDeserializer(cachedSchemaRegistryClient, props);
}
<http://log.info|log.info>("schema caching");
if (value.length < 4) {
log.error("length < 4");
return null;
}
// schema ID is bytes 1 to 4, byte 0 is magic byte.
ByteBuffer schemaIdBuffer = ByteBuffer.wrap(value, 1, 4);
Integer schemaId = schemaIdBuffer.getInt();
<http://log.info|log.info>("schema id: {}", schemaId);
long startTime = System.currentTimeMillis();
Schema schema = cachedSchemaRegistryClient.getBySubjectAndID(sourceTopic+"-value", schemaId);
long endTime = System.currentTimeMillis();
<http://log.info|log.info>("got the schema: {} with id: {}, time taken: {}ms", schema, schemaId, endTime-startTime);
return schema;
}
@Override
public TypeInformation<GenericRecord> getProducedType() {
return TypeExtractor.getForClass(GenericRecord.class);
}
}
For your FlinkKafkaConsumer client instance:
FlinkKafkaConsumer<GenericRecord> consumer = new FlinkKafkaConsumer<>(
sourceTopic,
new KafkaGenericAvroDeserializationSchema(REGISTRY_URL, sourceTopic),
kafkaProperties);
consumer.setStartFromEarliest();
DataStream<GenericRecord> consumerStream = env.addSource(consumer);
To avoid Kryo deserialization exceptions, add the following :
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
...
AvroKryoSerializerUtils avroKryoSerializerUtil = new AvroKryoSerializerUtils();
avroKryoSerializerUtil.addAvroSerializersIfRequired(env.getConfig(), GenericData.Record.class);
env.getConfig().enableForceAvro();Reme Ajayi
01/21/2023, 11:10 PMReme Ajayi
01/21/2023, 11:11 PMpackage org.flink.test;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.avro.generic.GenericRecord;
import org.apache.flink.formats.avro.registry.confluent.ConfluentRegistryAvroDeserializationSchema;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.json.JSONObject;
public class DataStreamJob {
public static void main(String[] args) throws Exception {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
final String registryUrl = "<http://localhost:8081>";
final String myConfluentSchema = "";
KafkaSource<GenericRecord> source = KafkaSource.<GenericRecord>builder()
.setBootstrapServers("broker:29092")
.setTopics("pageviews")
.setGroupId("test-001")
.setStartingOffsets(OffsetsInitializer.earliest())
.setValueOnlyDeserializer(ConfluentRegistryAvroDeserializationSchema.forGeneric(myConfluentSchema ,registryUrl))
.build();
// to connect later to cloud poc data
// KafkaSource.builder()
// .setProperty("security.protocol", "SASL_PLAINTEXT")
// .setProperty("sasl.mechanism", "PLAIN")
// .setProperty("sasl.jaas.config", "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"username\" password=\"password\";");
env.fromSource(source, WatermarkStrategy.noWatermarks(), "Kafka Source");
env.print();
env.execute("Kafka Source");
}
}Reme Ajayi
01/21/2023, 11:13 PM{
"connect.name": "ksql.pageviews",
"fields": [
{
"name": "viewtime",
"type": "long"
},
{
"name": "userid",
"type": "string"
},
{
"name": "pageid",
"type": "string"
}
],
"name": "pageviews",
"namespace": "ksql",
"type": "record"
}
I got this from Confluent Cloud, as this is where I'm trying to read data from. I don't know how to convert it to the schema that the forGeneric method requires. The documentation says it should be of type org.apache.schema , do you know how I can do that conversion?Guruguha Marur Sreenivasa
01/22/2023, 4:09 PMReme Ajayi
01/23/2023, 3:43 AM