All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.debezium.pipeline.EventDispatcher Maven / Gradle / Ivy

/*
 * Copyright Debezium Authors.
 *
 * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
 */
package io.debezium.pipeline;

import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;

import org.apache.kafka.connect.data.Schema;
import org.apache.kafka.connect.data.SchemaBuilder;
import org.apache.kafka.connect.data.Struct;
import org.apache.kafka.connect.errors.ConnectException;
import org.apache.kafka.connect.header.ConnectHeaders;
import org.apache.kafka.connect.source.SourceRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.debezium.config.CommonConnectorConfig;
import io.debezium.config.Configuration;
import io.debezium.connector.SnapshotRecord;
import io.debezium.connector.base.ChangeEventQueue;
import io.debezium.data.Envelope;
import io.debezium.data.Envelope.Operation;
import io.debezium.heartbeat.Heartbeat;
import io.debezium.pipeline.signal.Signal;
import io.debezium.pipeline.source.spi.DataChangeEventListener;
import io.debezium.pipeline.source.spi.EventMetadataProvider;
import io.debezium.pipeline.spi.ChangeEventCreator;
import io.debezium.pipeline.spi.ChangeRecordEmitter;
import io.debezium.pipeline.spi.ChangeRecordEmitter.Receiver;
import io.debezium.pipeline.spi.OffsetContext;
import io.debezium.pipeline.spi.SchemaChangeEventEmitter;
import io.debezium.pipeline.txmetadata.TransactionMonitor;
import io.debezium.relational.history.ConnectTableChangeSerializer;
import io.debezium.relational.history.HistoryRecord.Fields;
import io.debezium.relational.history.TableChanges.TableChangesSerializer;
import io.debezium.schema.DataCollectionFilters.DataCollectionFilter;
import io.debezium.schema.DataCollectionId;
import io.debezium.schema.DataCollectionSchema;
import io.debezium.schema.DatabaseSchema;
import io.debezium.schema.HistorizedDatabaseSchema;
import io.debezium.schema.SchemaChangeEvent;
import io.debezium.schema.TopicSelector;
import io.debezium.util.SchemaNameAdjuster;

/**
 * Central dispatcher for data change and schema change events. The former will be routed to the change event queue, the
 * latter will be routed to the {@link DatabaseSchema}. But based on the applying include/exclude list configuration,
 * events may be not be dispatched at all.
 * 

* This router is also in charge of emitting heartbeat messages, exposing of metrics via JMX etc. * * @author Gunnar Morling */ public class EventDispatcher { private static final Logger LOGGER = LoggerFactory.getLogger(EventDispatcher.class); private final TopicSelector topicSelector; private final DatabaseSchema schema; private final HistorizedDatabaseSchema historizedSchema; private final ChangeEventQueue queue; private final DataCollectionFilter filter; private final ChangeEventCreator changeEventCreator; private final Heartbeat heartbeat; private DataChangeEventListener eventListener = DataChangeEventListener.NO_OP; private final boolean emitTombstonesOnDelete; private final InconsistentSchemaHandler inconsistentSchemaHandler; private final TransactionMonitor transactionMonitor; private final CommonConnectorConfig connectorConfig; private final Schema schemaChangeKeySchema; private final Schema schemaChangeValueSchema; private final TableChangesSerializer> tableChangesSerializer = new ConnectTableChangeSerializer(); private final Signal signal; /** * Change event receiver for events dispatched from a streaming change event source. */ private final StreamingChangeRecordReceiver streamingReceiver; public EventDispatcher(CommonConnectorConfig connectorConfig, TopicSelector topicSelector, DatabaseSchema schema, ChangeEventQueue queue, DataCollectionFilter filter, ChangeEventCreator changeEventCreator, EventMetadataProvider metadataProvider, SchemaNameAdjuster schemaNameAdjuster) { this(connectorConfig, topicSelector, schema, queue, filter, changeEventCreator, null, metadataProvider, null, schemaNameAdjuster); } public EventDispatcher(CommonConnectorConfig connectorConfig, TopicSelector topicSelector, DatabaseSchema schema, ChangeEventQueue queue, DataCollectionFilter filter, ChangeEventCreator changeEventCreator, EventMetadataProvider metadataProvider, Heartbeat heartbeat, SchemaNameAdjuster schemaNameAdjuster) { this(connectorConfig, topicSelector, schema, queue, filter, changeEventCreator, null, metadataProvider, heartbeat, schemaNameAdjuster); } public EventDispatcher(CommonConnectorConfig connectorConfig, TopicSelector topicSelector, DatabaseSchema schema, ChangeEventQueue queue, DataCollectionFilter filter, ChangeEventCreator changeEventCreator, InconsistentSchemaHandler inconsistentSchemaHandler, EventMetadataProvider metadataProvider, Heartbeat customHeartbeat, SchemaNameAdjuster schemaNameAdjuster) { this.connectorConfig = connectorConfig; this.topicSelector = topicSelector; this.schema = schema; this.historizedSchema = schema instanceof HistorizedDatabaseSchema ? (HistorizedDatabaseSchema) schema : null; this.queue = queue; this.filter = filter; this.changeEventCreator = changeEventCreator; this.streamingReceiver = new StreamingChangeRecordReceiver(); this.emitTombstonesOnDelete = connectorConfig.isEmitTombstoneOnDelete(); this.inconsistentSchemaHandler = inconsistentSchemaHandler != null ? inconsistentSchemaHandler : this::errorOnMissingSchema; this.transactionMonitor = new TransactionMonitor(connectorConfig, metadataProvider, this::enqueueTransactionMessage); this.signal = new Signal(connectorConfig, this); if (customHeartbeat != null) { heartbeat = customHeartbeat; } else { Configuration configuration = connectorConfig.getConfig(); heartbeat = Heartbeat.create( configuration.getDuration(Heartbeat.HEARTBEAT_INTERVAL, ChronoUnit.MILLIS), topicSelector.getHeartbeatTopic(), connectorConfig.getLogicalName()); } schemaChangeKeySchema = SchemaBuilder.struct() .name(schemaNameAdjuster.adjust("io.debezium.connector." + connectorConfig.getConnectorName() + ".SchemaChangeKey")) .field(Fields.DATABASE_NAME, Schema.STRING_SCHEMA) .build(); schemaChangeValueSchema = SchemaBuilder.struct() .name(schemaNameAdjuster.adjust("io.debezium.connector." + connectorConfig.getConnectorName() + ".SchemaChangeValue")) .field(Fields.SOURCE, connectorConfig.getSourceInfoStructMaker().schema()) .field(Fields.DATABASE_NAME, Schema.OPTIONAL_STRING_SCHEMA) .field(Fields.SCHEMA_NAME, Schema.OPTIONAL_STRING_SCHEMA) .field(Fields.DDL_STATEMENTS, Schema.OPTIONAL_STRING_SCHEMA) .field(Fields.TABLE_CHANGES, SchemaBuilder.array(ConnectTableChangeSerializer.CHANGE_SCHEMA).build()) .build(); } public void dispatchSnapshotEvent(T dataCollectionId, ChangeRecordEmitter changeRecordEmitter, SnapshotReceiver receiver) throws InterruptedException { // TODO Handle Heartbeat DataCollectionSchema dataCollectionSchema = schema.schemaFor(dataCollectionId); // TODO handle as per inconsistent schema info option if (dataCollectionSchema == null) { errorOnMissingSchema(dataCollectionId, changeRecordEmitter); } changeRecordEmitter.emitChangeRecords(dataCollectionSchema, new Receiver() { @Override public void changeRecord(DataCollectionSchema schema, Operation operation, Object key, Struct value, OffsetContext offset, ConnectHeaders headers) throws InterruptedException { eventListener.onEvent(dataCollectionSchema.id(), offset, key, value); receiver.changeRecord(dataCollectionSchema, operation, key, value, offset, headers); } }); } public SnapshotReceiver getSnapshotChangeEventReceiver() { return new BufferingSnapshotChangeRecordReceiver(); } /** * Dispatches one or more {@link DataChangeEvent}s. If the given data collection is included in the currently * captured set of collections, the given emitter will be invoked, so it can emit one or more events (in the common * case, one event will be emitted, but e.g. in case of PK updates, it may be a deletion and a creation event). The * receiving coordinator creates {@link SourceRecord}s for all emitted events and passes them to this dispatcher's * {@link ChangeEventCreator} for converting them into data change events. * * @return {@code true} if an event was dispatched (i.e. sent to the message broker), {@code false} otherwise. */ public boolean dispatchDataChangeEvent(T dataCollectionId, ChangeRecordEmitter changeRecordEmitter) throws InterruptedException { try { boolean handled = false; if (!filter.isIncluded(dataCollectionId)) { LOGGER.trace("Filtered data change event for {}", dataCollectionId); eventListener.onFilteredEvent("source = " + dataCollectionId); } else { DataCollectionSchema dataCollectionSchema = schema.schemaFor(dataCollectionId); // TODO handle as per inconsistent schema info option if (dataCollectionSchema == null) { final Optional replacementSchema = inconsistentSchemaHandler.handle(dataCollectionId, changeRecordEmitter); if (!replacementSchema.isPresent()) { return false; } dataCollectionSchema = replacementSchema.get(); } changeRecordEmitter.emitChangeRecords(dataCollectionSchema, new Receiver() { @Override public void changeRecord(DataCollectionSchema schema, Operation operation, Object key, Struct value, OffsetContext offset, ConnectHeaders headers) throws InterruptedException { transactionMonitor.dataEvent(dataCollectionId, offset, key, value); eventListener.onEvent(dataCollectionId, offset, key, value); if (operation == Operation.CREATE && signal.isSignal(dataCollectionId)) { signal.process(value, offset); } streamingReceiver.changeRecord(schema, operation, key, value, offset, headers); } }); handled = true; } heartbeat.heartbeat( changeRecordEmitter.getOffset().getPartition(), changeRecordEmitter.getOffset().getOffset(), this::enqueueHeartbeat); return handled; } catch (Exception e) { switch (connectorConfig.getEventProcessingFailureHandlingMode()) { case FAIL: throw new ConnectException("Error while processing event at offset " + changeRecordEmitter.getOffset().getOffset(), e); case WARN: LOGGER.warn( "Error while processing event at offset {}", changeRecordEmitter.getOffset().getOffset()); break; case SKIP: LOGGER.debug( "Error while processing event at offset {}", changeRecordEmitter.getOffset().getOffset()); break; } return false; } } public void dispatchTransactionCommittedEvent(OffsetContext offset) throws InterruptedException { transactionMonitor.transactionComittedEvent(offset); } public void dispatchTransactionStartedEvent(String transactionId, OffsetContext offset) throws InterruptedException { transactionMonitor.transactionStartedEvent(transactionId, offset); } public void dispatchConnectorEvent(ConnectorEvent event) { eventListener.onConnectorEvent(event); } public Optional errorOnMissingSchema(T dataCollectionId, ChangeRecordEmitter changeRecordEmitter) { eventListener.onErroneousEvent("source = " + dataCollectionId); throw new IllegalArgumentException("No metadata registered for captured table " + dataCollectionId); } public Optional ignoreMissingSchema(T dataCollectionId, ChangeRecordEmitter changeRecordEmitter) { return Optional.empty(); } public void dispatchSchemaChangeEvent(T dataCollectionId, SchemaChangeEventEmitter schemaChangeEventEmitter) throws InterruptedException { if (dataCollectionId != null && !filter.isIncluded(dataCollectionId)) { if (historizedSchema == null || historizedSchema.storeOnlyMonitoredTables()) { LOGGER.trace("Filtering schema change event for {}", dataCollectionId); return; } } schemaChangeEventEmitter.emitSchemaChangeEvent(new SchemaChangeEventReceiver()); } public void dispatchSchemaChangeEvent(Collection dataCollectionIds, SchemaChangeEventEmitter schemaChangeEventEmitter) throws InterruptedException { boolean anyNonfilteredEvent = false; if (dataCollectionIds == null || dataCollectionIds.isEmpty()) { anyNonfilteredEvent = true; } else { for (T dataCollectionId : dataCollectionIds) { if (filter.isIncluded(dataCollectionId)) { anyNonfilteredEvent = true; break; } } } if (!anyNonfilteredEvent) { if (historizedSchema == null || historizedSchema.storeOnlyMonitoredTables()) { LOGGER.trace("Filtering schema change event for {}", dataCollectionIds); return; } } schemaChangeEventEmitter.emitSchemaChangeEvent(new SchemaChangeEventReceiver()); } public void alwaysDispatchHeartbeatEvent(OffsetContext offset) throws InterruptedException { heartbeat.forcedBeat( offset.getPartition(), offset.getOffset(), this::enqueueHeartbeat); } public void dispatchHeartbeatEvent(OffsetContext offset) throws InterruptedException { heartbeat.heartbeat( offset.getPartition(), offset.getOffset(), this::enqueueHeartbeat); } public boolean heartbeatsEnabled() { return heartbeat.isEnabled(); } private void enqueueHeartbeat(SourceRecord record) throws InterruptedException { queue.enqueue(new DataChangeEvent(record)); } private void enqueueTransactionMessage(SourceRecord record) throws InterruptedException { queue.enqueue(new DataChangeEvent(record)); } private void enqueueSchemaChangeMessage(SourceRecord record) throws InterruptedException { queue.enqueue(new DataChangeEvent(record)); } /** * Change record receiver used during snapshotting. Allows for a deferred submission of records, which is needed in * order to set the "snapshot completed" offset field, which we can't send to Kafka Connect without sending an * actual record */ public interface SnapshotReceiver extends ChangeRecordEmitter.Receiver { void completeSnapshot() throws InterruptedException; } private final class StreamingChangeRecordReceiver implements ChangeRecordEmitter.Receiver { @Override public void changeRecord(DataCollectionSchema dataCollectionSchema, Operation operation, Object key, Struct value, OffsetContext offsetContext, ConnectHeaders headers) throws InterruptedException { Objects.requireNonNull(value, "value must not be null"); LOGGER.trace("Received change record for {} operation on key {}", operation, key); // Truncate events must have null key schema as they are sent to table topics without keys Schema keySchema = (key == null && operation == Operation.TRUNCATE) ? null : dataCollectionSchema.keySchema(); String topicName = topicSelector.topicNameFor((T) dataCollectionSchema.id()); SourceRecord record = new SourceRecord(offsetContext.getPartition(), offsetContext.getOffset(), topicName, null, keySchema, key, dataCollectionSchema.getEnvelopeSchema().schema(), value, null, headers); queue.enqueue(changeEventCreator.createDataChangeEvent(record)); if (emitTombstonesOnDelete && operation == Operation.DELETE) { SourceRecord tombStone = record.newRecord( record.topic(), record.kafkaPartition(), record.keySchema(), record.key(), null, // value schema null, // value record.timestamp(), record.headers()); queue.enqueue(changeEventCreator.createDataChangeEvent(tombStone)); } } } private final class BufferingSnapshotChangeRecordReceiver implements SnapshotReceiver { private Supplier bufferedEvent; @Override public void changeRecord(DataCollectionSchema dataCollectionSchema, Operation operation, Object key, Struct value, OffsetContext offsetContext, ConnectHeaders headers) throws InterruptedException { Objects.requireNonNull(value, "value must not be null"); LOGGER.trace("Received change record for {} operation on key {}", operation, key); if (bufferedEvent != null) { queue.enqueue(bufferedEvent.get()); } Schema keySchema = dataCollectionSchema.keySchema(); String topicName = topicSelector.topicNameFor((T) dataCollectionSchema.id()); // the record is produced lazily, so to have the correct offset as per the pre/post completion callbacks bufferedEvent = () -> { SourceRecord record = new SourceRecord( offsetContext.getPartition(), offsetContext.getOffset(), topicName, null, keySchema, key, dataCollectionSchema.getEnvelopeSchema().schema(), value, null, headers); return changeEventCreator.createDataChangeEvent(record); }; } @Override public void completeSnapshot() throws InterruptedException { if (bufferedEvent != null) { // It is possible that the last snapshotted table was empty // this way we ensure that the last event is always marked as last // even if it originates form non-last table final DataChangeEvent event = bufferedEvent.get(); final Struct envelope = (Struct) event.getRecord().value(); if (envelope.schema().field(Envelope.FieldName.SOURCE) != null) { final Struct source = envelope.getStruct(Envelope.FieldName.SOURCE); final SnapshotRecord snapshot = SnapshotRecord.fromSource(source); if (snapshot == SnapshotRecord.TRUE) { SnapshotRecord.LAST.toSource(source); } } queue.enqueue(event); bufferedEvent = null; } } } private final class SchemaChangeEventReceiver implements SchemaChangeEventEmitter.Receiver { private Struct schemaChangeRecordKey(SchemaChangeEvent event) { Struct result = new Struct(schemaChangeKeySchema); result.put(Fields.DATABASE_NAME, event.getDatabase()); return result; } private Struct schemaChangeRecordValue(SchemaChangeEvent event) { Struct result = new Struct(schemaChangeValueSchema); result.put(Fields.SOURCE, event.getSource()); result.put(Fields.DATABASE_NAME, event.getDatabase()); result.put(Fields.SCHEMA_NAME, event.getSchema()); result.put(Fields.DDL_STATEMENTS, event.getDdl()); result.put(Fields.TABLE_CHANGES, tableChangesSerializer.serialize(event.getTableChanges())); return result; } @Override public void schemaChangeEvent(SchemaChangeEvent event) throws InterruptedException { historizedSchema.applySchemaChange(event); if (connectorConfig.isSchemaChangesHistoryEnabled()) { final String topicName = topicSelector.getPrimaryTopic(); final Integer partition = 0; final Struct key = schemaChangeRecordKey(event); final Struct value = schemaChangeRecordValue(event); final SourceRecord record = new SourceRecord(event.getPartition(), event.getOffset(), topicName, partition, schemaChangeKeySchema, key, schemaChangeValueSchema, value); enqueueSchemaChangeMessage(record); } } } /** * Provide a listener that is invoked for every incoming event to be processed. * * @param eventListener */ public void setEventListener(DataChangeEventListener eventListener) { this.eventListener = eventListener; } /** * Reaction to an incoming change event for which schema is not found */ @FunctionalInterface public static interface InconsistentSchemaHandler { /** * @return collection schema if the schema was updated and event can be processed, {@code empty} to skip the processing */ Optional handle(T dataCollectionId, ChangeRecordEmitter changeRecordEmitter); } public DatabaseSchema getSchema() { return schema; } public HistorizedDatabaseSchema getHistorizedSchema() { return historizedSchema; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy