use of io.debezium.embedded.EmbeddedEngine.CompletionCallback in project debezium by debezium.
the class ConnectorOutputTest method runConnector.
/**
* Run the connector described by the supplied test specification.
*
* @param spec the test specification
* @param callback the function that should be called when the connector is stopped
*/
protected void runConnector(TestSpecification spec, CompletionCallback callback) {
PreviousContext preRunContext = LoggingContext.forConnector(getClass().getSimpleName(), "runner", spec.name());
final Configuration environmentConfig = Configuration.copy(spec.environment()).build();
final Configuration connectorConfig = spec.config();
String[] ignorableFieldNames = environmentConfig.getString(ENV_IGNORE_FIELDS, "").split(",");
final Set<String> ignorableFields = Arrays.stream(ignorableFieldNames).map(String::trim).collect(Collectors.toSet());
String[] globallyIgnorableFieldNames = globallyIgnorableFieldNames();
if (globallyIgnorableFieldNames != null && globallyIgnorableFieldNames.length != 0) {
ignorableFields.addAll(Arrays.stream(globallyIgnorableFieldNames).map(String::trim).collect(Collectors.toSet()));
}
final SchemaAndValueConverter keyConverter = new SchemaAndValueConverter(environmentConfig, true);
final SchemaAndValueConverter valueConverter = new SchemaAndValueConverter(environmentConfig, false);
final TestData testData = spec.testData();
// Get any special comparators ...
final Map<String, RecordValueComparator> comparatorsByFieldName = new HashMap<>();
addValueComparatorsByFieldPath(comparatorsByFieldName::put);
final Map<String, RecordValueComparator> comparatorsBySchemaName = new HashMap<>();
addValueComparatorsBySchemaName(comparatorsBySchemaName::put);
RuntimeException runError = null;
CompletionResult problem = new CompletionResult(callback);
try {
// Set up the test data ...
final PreviewIterator<Document> expectedRecords = Iterators.preview(testData.read());
final Consumer<Document> recorder = testData::write;
// We need something that will measure the amount of time since our consumer has seen a record ...
TimeSince timeSinceLastRecord = Threads.timeSince(Clock.SYSTEM);
// We'll keep the last 10 expected and actual records so that there is some context if they don't match ...
Queue<SourceRecord> actualRecordHistory = fixedSizeQueue(10);
Queue<SourceRecord> expectedRecordHistory = fixedSizeQueue(10);
// Define what happens for each record ...
ConsumerCompletion result = new ConsumerCompletion();
Consumer<SourceRecord> consumer = (actualRecord) -> {
PreviousContext prev = LoggingContext.forConnector(getClass().getSimpleName(), "runner", spec.name());
try {
Testing.debug("actual record: " + SchemaUtil.asString(actualRecord));
timeSinceLastRecord.reset();
// Record the actual in the history ...
actualRecordHistory.add(actualRecord);
// And possibly hand it to the test's recorder ...
try {
Document jsonRecord = serializeSourceRecord(actualRecord, keyConverter, valueConverter);
if (jsonRecord != null)
recorder.accept(jsonRecord);
} catch (IOException e) {
String msg = "Error converting JSON to SourceRecord";
Testing.debug(msg);
throw new ConnectException(msg, e);
}
if (expectedRecords != null) {
// Get the test's next expected record ...
if (!expectedRecords.hasNext()) {
// We received an actual record but don't have or expect one ...
String msg = "Source record found but nothing expected";
result.error();
Testing.debug(msg);
throw new MismatchRecordException(msg, actualRecordHistory, expectedRecordHistory);
}
Document expected = expectedRecords.next();
if (isEndCommand(expected)) {
result.error();
String msg = "Source record was found but not expected: " + SchemaUtil.asString(actualRecord);
Testing.debug(msg);
throw new MismatchRecordException(msg, actualRecordHistory, expectedRecordHistory);
} else if (isCommand(expected)) {
Testing.debug("applying command: " + SchemaUtil.asString(expected));
applyCommand(expected, result);
} else {
try {
// Otherwise, build a record from the expected and add it to the history ...
SourceRecord expectedRecord = rehydrateSourceRecord(expected, keyConverter, valueConverter);
expectedRecordHistory.add(expectedRecord);
Testing.debug("expected record: " + SchemaUtil.asString(expectedRecord));
// And compare the records ...
try {
assertSourceRecordMatch(actualRecord, expectedRecord, ignorableFields::contains, comparatorsByFieldName, comparatorsBySchemaName);
} catch (AssertionError e) {
result.error();
String msg = "Source record with key " + SchemaUtil.asString(actualRecord.key()) + " did not match expected record: " + e.getMessage();
Testing.debug(msg);
throw new MismatchRecordException(e, msg, actualRecordHistory, expectedRecordHistory);
}
} catch (IOException e) {
result.exception();
String msg = "Error converting JSON to SourceRecord";
Testing.debug(msg);
throw new ConnectException(msg, e);
}
}
if (!expectedRecords.hasNext()) {
// We expect no more records, so stop the connector ...
result.stop();
String msg = "Stopping connector after no more expected records found";
Testing.debug(msg);
throw new StopConnectorException(msg);
}
// Peek at the next record to see if it is a command ...
Document nextExpectedRecord = expectedRecords.peek();
if (isCommand(nextExpectedRecord)) {
// consume it and apply it ...
applyCommand(expectedRecords.next(), result);
}
}
} finally {
prev.restore();
}
};
// Set up the configuration for the engine to include the connector configuration and apply as defaults
// the environment and engine parameters ...
Configuration engineConfig = Configuration.copy(connectorConfig).withDefault(environmentConfig).withDefault(EmbeddedEngine.ENGINE_NAME, spec.name()).withDefault(StandaloneConfig.OFFSET_STORAGE_FILE_FILENAME_CONFIG, OFFSET_STORE_PATH).withDefault(EmbeddedEngine.OFFSET_FLUSH_INTERVAL_MS, 0).build();
// Create the engine ...
EmbeddedEngine engine = EmbeddedEngine.create().using(engineConfig).notifying(consumer).using(this.getClass().getClassLoader()).using(problem).build();
long connectorTimeoutInSeconds = environmentConfig.getLong(ENV_CONNECTOR_TIMEOUT_IN_SECONDS, 10);
// Get ready to run the connector one or more times ...
do {
// Each time create a thread that will stop our connector if we don't get enough results
Thread timeoutThread = Threads.timeout(spec.name() + "-connector-output", connectorTimeoutInSeconds, TimeUnit.SECONDS, timeSinceLastRecord, engine::stop);
// But plan to stop our timeout thread as soon as the connector completes ...
result.uponCompletion(timeoutThread::interrupt);
timeoutThread.start();
// Run the connector and block until the connector is stopped by the timeout thread or until
// an exception is thrown within the connector (perhaps by the consumer) ...
Testing.debug("Starting connector");
result.reset();
engine.run();
} while (result.get() == ExecutionResult.RESTART_REQUESTED);
} catch (IOException e) {
runError = new RuntimeException("Error reading test data: " + e.getMessage(), e);
} catch (RuntimeException t) {
runError = t;
} finally {
// And clean up everything ...
try {
testData.close();
} catch (IOException e) {
if (runError != null) {
runError = new RuntimeException("Error closing test data: " + e.getMessage(), e);
}
} finally {
try {
keyConverter.close();
} finally {
try {
valueConverter.close();
} finally {
preRunContext.restore();
}
}
}
}
if (runError != null) {
throw runError;
}
if (problem.hasError()) {
Throwable error = problem.error();
if (error instanceof AssertionError) {
fail(problem.message());
} else if (error instanceof MismatchRecordException) {
MismatchRecordException mismatch = (MismatchRecordException) error;
LinkedList<SourceRecord> actualHistory = mismatch.getActualRecords();
LinkedList<SourceRecord> expectedHistory = mismatch.getExpectedRecords();
Testing.print("");
Testing.print("FAILURE in connector integration test '" + spec.name() + "' in class " + getClass());
Testing.print(" actual record: " + SchemaUtil.asString(actualHistory.getLast()));
Testing.print(" expected record: " + SchemaUtil.asString(expectedHistory.getLast()));
Testing.print(mismatch.getMessage());
Testing.print("");
AssertionError cause = ((MismatchRecordException) error).getError();
if (cause != null) {
throw cause;
}
fail(problem.message());
} else if (error instanceof RuntimeException) {
throw (RuntimeException) error;
} else {
throw new RuntimeException(error);
}
}
}
use of io.debezium.embedded.EmbeddedEngine.CompletionCallback in project debezium by debezium.
the class AbstractConnectorTest method start.
/**
* Start the connector using the supplied connector configuration.
*
* @param connectorClass the connector class; may not be null
* @param connectorConfig the configuration for the connector; may not be null
* @param isStopRecord the function that will be called to determine if the connector should be stopped before processing
* this record; may be null if not needed
* @param callback the function that will be called when the engine fails to start the connector or when the connector
* stops running after completing successfully or due to an error; may be null
*/
protected void start(Class<? extends SourceConnector> connectorClass, Configuration connectorConfig, CompletionCallback callback, Predicate<SourceRecord> isStopRecord) {
Configuration config = Configuration.copy(connectorConfig).with(EmbeddedEngine.ENGINE_NAME, "testing-connector").with(EmbeddedEngine.CONNECTOR_CLASS, connectorClass.getName()).with(StandaloneConfig.OFFSET_STORAGE_FILE_FILENAME_CONFIG, OFFSET_STORE_PATH).with(EmbeddedEngine.OFFSET_FLUSH_INTERVAL_MS, 0).build();
latch = new CountDownLatch(1);
CompletionCallback wrapperCallback = (success, msg, error) -> {
try {
if (callback != null)
callback.handle(success, msg, error);
} finally {
if (!success) {
// we only unblock if there was an error; in all other cases we're unblocking when a task has been started
latch.countDown();
}
}
Testing.debug("Stopped connector");
};
ConnectorCallback connectorCallback = new ConnectorCallback() {
@Override
public void taskStarted() {
// if this is called, it means a task has been started successfully so we can continue
latch.countDown();
}
};
// Create the connector ...
engine = EmbeddedEngine.create().using(config).notifying((record) -> {
if (isStopRecord != null && isStopRecord.test(record)) {
logger.error("Stopping connector after record as requested");
throw new ConnectException("Stopping connector after record as requested");
}
try {
consumedLines.put(record);
} catch (InterruptedException e) {
Thread.interrupted();
}
}).using(this.getClass().getClassLoader()).using(wrapperCallback).using(connectorCallback).build();
// Submit the connector for asynchronous execution ...
assertThat(executor).isNull();
executor = Executors.newFixedThreadPool(1);
executor.execute(() -> {
LoggingContext.forConnector(getClass().getSimpleName(), "", "engine");
engine.run();
});
try {
if (!latch.await(10, TimeUnit.SECONDS)) {
// maybe it takes more time to start up, so just log a warning and continue
logger.warn("The connector did not finish starting its task(s) or complete in the expected amount of time");
}
} catch (InterruptedException e) {
if (Thread.interrupted()) {
fail("Interrupted while waiting for engine startup");
}
}
}
Aggregations