This is an automated email from the ASF dual-hosted git repository.
david-streamlio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar-connectors.git
The following commit(s) were added to refs/heads/master by this push:
new e0f878ce [fix][test] Bound record reads in Debezium MySQL and Postgres
tests (#71)
e0f878ce is described below
commit e0f878ce84e4b89d116bd4f58182f72bd7c3f595
Author: David Kjerrumgaard <[email protected]>
AuthorDate: Thu Jul 9 14:44:18 2026 -0700
[fix][test] Bound record reads in Debezium MySQL and Postgres tests (#71)
* [fix][test] Bound record reads in Debezium MySQL and Postgres tests
AbstractKafkaConnectSource.read() loops until a record is available and
never returns null. Calling it on the test thread inside an Awaitility
untilAsserted block means a connector that produces fewer records than
expected blocks forever: Awaitility cannot interrupt a blocked
assertion, so its atMost(60s) never fires. Neither test declared a
timeOut, so the CI job ran to its own 45-minute limit and reported a
cancellation rather than a failure.
Read each record on a bounded worker thread and add a test-level
timeOut as a backstop, so under-delivery fails promptly with a message
naming the connector.
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
---------
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
---
.../io/debezium/mysql/DebeziumMysqlSourceTest.java | 72 +++++++++++++++-------
.../postgres/DebeziumPostgresSourceTest.java | 68 +++++++++++++-------
2 files changed, 98 insertions(+), 42 deletions(-)
diff --git
a/debezium/mysql/src/test/java/org/apache/pulsar/io/debezium/mysql/DebeziumMysqlSourceTest.java
b/debezium/mysql/src/test/java/org/apache/pulsar/io/debezium/mysql/DebeziumMysqlSourceTest.java
index b6ec3ed6..fa8f4f9a 100644
---
a/debezium/mysql/src/test/java/org/apache/pulsar/io/debezium/mysql/DebeziumMysqlSourceTest.java
+++
b/debezium/mysql/src/test/java/org/apache/pulsar/io/debezium/mysql/DebeziumMysqlSourceTest.java
@@ -21,20 +21,23 @@ package org.apache.pulsar.io.debezium.mysql;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertTrue;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import lombok.extern.slf4j.Slf4j;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.common.schema.KeyValue;
import org.apache.pulsar.functions.api.Record;
import org.apache.pulsar.io.core.SourceContext;
-import org.awaitility.Awaitility;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.PulsarContainer;
import org.testcontainers.containers.wait.strategy.Wait;
@@ -49,15 +52,26 @@ public class DebeziumMysqlSourceTest {
private static final String PULSAR_IMAGE =
System.getenv().getOrDefault("PULSAR_TEST_IMAGE",
"apachepulsar/pulsar:4.1.3");
+ /** Rows seeded before open(), each of which the initial snapshot must
emit. */
+ private static final int EXPECTED_RECORDS = 2;
+
+ private static final int READ_TIMEOUT_SECONDS = 120;
+
private static final int MYSQL_PORT = 3306;
private GenericContainer<?> mysqlContainer;
private PulsarContainer pulsarContainer;
private PulsarClient pulsarClient;
private DebeziumMysqlSource source;
+ private ExecutorService readerExecutor;
@BeforeMethod
public void setup() throws Exception {
+readerExecutor = Executors.newSingleThreadExecutor(r -> {
+ Thread t = new Thread(r, "debezium-mysql-test-reader");
+ t.setDaemon(true);
+ return t;
+});
// Use GenericContainer instead of MySQLContainer to get full root
access
// which is required for Debezium's REPLICATION SLAVE/CLIENT privileges
mysqlContainer = new
GenericContainer<>(DockerImageName.parse("mysql:8.0"))
@@ -101,6 +115,10 @@ public class DebeziumMysqlSourceTest {
@AfterMethod(alwaysRun = true)
public void cleanup() throws Exception {
+ if (readerExecutor != null) {
+ // shutdownNow: a reader may still be blocked in read(), which
never returns null
+ readerExecutor.shutdownNow();
+ }
if (source != null) {
try {
source.close();
@@ -119,7 +137,7 @@ public class DebeziumMysqlSourceTest {
}
}
- @Test
+ @Test(timeOut = 600_000)
public void testMysqlCdcEvents() throws Exception {
String pulsarServiceUrl = pulsarContainer.getPulsarBrokerUrl();
@@ -147,23 +165,35 @@ public class DebeziumMysqlSourceTest {
// Debezium performs an initial snapshot of existing data.
// We should receive CDC records for the 2 rows we inserted.
- Awaitility.await()
- .atMost(60, TimeUnit.SECONDS)
- .pollInterval(500, TimeUnit.MILLISECONDS)
- .untilAsserted(() -> {
- int recordCount = 0;
- Record<KeyValue<byte[], byte[]>> record;
- while ((record = source.read()) != null) {
- assertNotNull(record.getValue());
- log.info("Received CDC record: key={}",
record.getKey().orElse(null));
- recordCount++;
- record.ack();
- if (recordCount >= 2) {
- break;
- }
- }
- assertTrue(recordCount >= 2,
- "Expected at least 2 CDC records from initial
snapshot, got " + recordCount);
- });
+ int received = 0;
+ for (int i = 0; i < EXPECTED_RECORDS; i++) {
+ Record<KeyValue<byte[], byte[]>> record = readOne();
+ assertNotNull(record.getValue());
+ log.info("Received CDC record: key={}",
record.getKey().orElse(null));
+ record.ack();
+ received++;
+ }
+ assertEquals(received, EXPECTED_RECORDS);
+ }
+
+ /**
+ * Reads a single record, failing if none arrives in time.
+ *
+ * <p>{@code AbstractKafkaConnectSource.read()} loops until a record is
available and never
+ * returns null, so calling it on the test thread means an
under-delivering connector hangs
+ * the test — and, since Awaitility cannot interrupt a blocked assertion,
the whole CI job
+ * until its own timeout. Run it on a separate thread so a missing record
surfaces as a
+ * prompt, diagnosable failure instead.
+ */
+ private Record<KeyValue<byte[], byte[]>> readOne() throws Exception {
+ Future<Record<KeyValue<byte[], byte[]>>> future =
readerExecutor.submit(() -> source.read());
+ try {
+ return future.get(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ } catch (TimeoutException e) {
+ future.cancel(true);
+ throw new AssertionError("Timed out after " + READ_TIMEOUT_SECONDS
+ + "s waiting for a CDC record from the initial snapshot. "
+ + "The connector produced no record; see the Debezium logs
above.", e);
+ }
}
}
diff --git
a/debezium/postgres/src/test/java/org/apache/pulsar/io/debezium/postgres/DebeziumPostgresSourceTest.java
b/debezium/postgres/src/test/java/org/apache/pulsar/io/debezium/postgres/DebeziumPostgresSourceTest.java
index 836a7161..f550d8e9 100644
---
a/debezium/postgres/src/test/java/org/apache/pulsar/io/debezium/postgres/DebeziumPostgresSourceTest.java
+++
b/debezium/postgres/src/test/java/org/apache/pulsar/io/debezium/postgres/DebeziumPostgresSourceTest.java
@@ -21,20 +21,23 @@ package org.apache.pulsar.io.debezium.postgres;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertTrue;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import lombok.extern.slf4j.Slf4j;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.common.schema.KeyValue;
import org.apache.pulsar.functions.api.Record;
import org.apache.pulsar.io.core.SourceContext;
-import org.awaitility.Awaitility;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.PulsarContainer;
import org.testcontainers.utility.DockerImageName;
@@ -48,13 +51,20 @@ public class DebeziumPostgresSourceTest {
private static final String PULSAR_IMAGE =
System.getenv().getOrDefault("PULSAR_TEST_IMAGE",
"apachepulsar/pulsar:4.1.3");
+ /** Rows seeded before open(), each of which the initial snapshot must
emit. */
+ private static final int EXPECTED_RECORDS = 2;
+
+ private static final int READ_TIMEOUT_SECONDS = 120;
+
private PostgreSQLContainer<?> postgresContainer;
private PulsarContainer pulsarContainer;
private PulsarClient pulsarClient;
private DebeziumPostgresSource source;
+ private ExecutorService readerExecutor;
@BeforeMethod
public void setup() throws Exception {
+ readerExecutor = Executors.newSingleThreadExecutor();
postgresContainer = new
PostgreSQLContainer<>(DockerImageName.parse("postgres:16"))
.withDatabaseName("testdb")
.withUsername("debezium")
@@ -89,6 +99,10 @@ public class DebeziumPostgresSourceTest {
@AfterMethod(alwaysRun = true)
public void cleanup() throws Exception {
+ if (readerExecutor != null) {
+ // shutdownNow: a reader may still be blocked in read(), which
never returns null
+ readerExecutor.shutdownNow();
+ }
if (source != null) {
try {
source.close();
@@ -107,7 +121,7 @@ public class DebeziumPostgresSourceTest {
}
}
- @Test
+ @Test(timeOut = 600_000)
public void testPostgresCdcEvents() throws Exception {
String pulsarServiceUrl = pulsarContainer.getPulsarBrokerUrl();
@@ -134,23 +148,35 @@ public class DebeziumPostgresSourceTest {
// Debezium performs an initial snapshot of existing data.
// We should receive CDC records for the 2 rows we inserted.
- Awaitility.await()
- .atMost(60, TimeUnit.SECONDS)
- .pollInterval(500, TimeUnit.MILLISECONDS)
- .untilAsserted(() -> {
- int recordCount = 0;
- Record<KeyValue<byte[], byte[]>> record;
- while ((record = source.read()) != null) {
- assertNotNull(record.getValue());
- log.info("Received CDC record: key={}",
record.getKey().orElse(null));
- recordCount++;
- record.ack();
- if (recordCount >= 2) {
- break;
- }
- }
- assertTrue(recordCount >= 2,
- "Expected at least 2 CDC records from initial
snapshot, got " + recordCount);
- });
+ int received = 0;
+ for (int i = 0; i < EXPECTED_RECORDS; i++) {
+ Record<KeyValue<byte[], byte[]>> record = readOne();
+ assertNotNull(record.getValue());
+ log.info("Received CDC record: key={}",
record.getKey().orElse(null));
+ record.ack();
+ received++;
+ }
+ assertEquals(received, EXPECTED_RECORDS);
+ }
+
+ /**
+ * Reads a single record, failing if none arrives in time.
+ *
+ * <p>{@code AbstractKafkaConnectSource.read()} loops until a record is
available and never
+ * returns null, so calling it on the test thread means an
under-delivering connector hangs
+ * the test — and, since Awaitility cannot interrupt a blocked assertion,
the whole CI job
+ * until its own timeout. Run it on a separate thread so a missing record
surfaces as a
+ * prompt, diagnosable failure instead.
+ */
+ private Record<KeyValue<byte[], byte[]>> readOne() throws Exception {
+ Future<Record<KeyValue<byte[], byte[]>>> future =
readerExecutor.submit(() -> source.read());
+ try {
+ return future.get(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ } catch (TimeoutException e) {
+ future.cancel(true);
+ throw new AssertionError("Timed out after " + READ_TIMEOUT_SECONDS
+ + "s waiting for a CDC record from the initial snapshot. "
+ + "The connector produced no record; see the Debezium logs
above.", e);
+ }
}
}