debezium / debezium/dbz

MySQL/MariaDB error handlers pass a null throwable to super.isRetriable(), so every error is non-retriable

Open
#2,611 0 comments 0 reactions 0 assignees View on GitHub
component/mariadb-connector component/mysql-connector type/bug
Dominant language
HTML
Stars
6
Forks
8
Avg merge
2d 19h
Merged PRs (30d)
1

Description

## Bug report

**What Debezium connector do you use and what version?**

`io.debezium.connector.mysql.MySqlConnector` and `io.debezium.connector.mariadb.MariaDbConnector`.

Found on 3.5.2.Final. Still present on `main`, on 3.6.2.Final and on 3.7.0.Beta1 (checked
today). The code is identical in both connectors.

---

**What is the connector configuration?**

Not configuration dependent — the defect is in `isRetriable`, which every configuration goes
through. A minimal MySQL / Debezium Server configuration reproduces it:

```properties
debezium.source.connector.class=io.debezium.connector.mysql.MySqlConnector
debezium.source.topic.prefix=example
debezium.source.database.hostname=
debezium.source.database.port=3306
debezium.source.database.user=
debezium.source.database.server.id=1001
debezium.source.database.include.list=inventory
```

---

**What is the captured database version and mode of deployment?**

MySQL 8.0 (self-managed, Debezium Server in a container). The defect is in Java code and does
not depend on the server version or the deployment.

---

**What behavior do you expect?**

`MySqlErrorHandler` and `MariaDbErrorHandler` override `communicationExceptions()` to return
`{IOException, SQLException}`. So a connection loss — for example a `java.io.EOFException`
when the server closes the binlog socket — should be retriable, and the engine should restart
the connector.

---

**What behavior do you see?**

Every error is non-retriable. `setProducerThrowable` raises `ConnectException`, the engine
stops, and the container exits.

The cause is in
[`MySqlErrorHandler.isRetriable`](https://github.com/debezium/debezium/blob/main/debezium-connector-mysql/src/main/java/io/debezium/connector/mysql/MySqlErrorHandler.java#L38-L49)
and the identical
[`MariaDbErrorHandler.isRetriable`](https://github.com/debezium/debezium/blob/main/debezium-connector-mariadb/src/main/java/io/debezium/connector/mariadb/MariaDbErrorHandler.java#L38-L49):

```java
@Override
protected boolean isRetriable(Throwable throwable) {
while (throwable != null) {
if (throwable instanceof SQLException sqlException) {
if (NON_RETRIABLE_ERROR_CODES.contains(sqlException.getErrorCode())) {
return false;
}
}
throwable = throwable.getCause();
}
return super.isRetriable(throwable); // throwable is always null here
}
```

The method reuses its own parameter as the loop cursor. The loop exits only when `throwable`
is `null`, so `super.isRetriable(throwable)` always receives `null`.
[`ErrorHandler.isRetriable`](https://github.com/debezium/debezium/blob/main/debezium-connector-common/src/main/java/io/debezium/pipeline/ErrorHandler.java#L85-L88)
returns `false` on `null`. The `communicationExceptions()` override is therefore dead code in
both connectors.

`isCustomRetriable` still runs, so `custom.retriable.exception` is a workaround. Without it,
a dropped binlog connection is a hard stop rather than a retry.

**Regression:** commit
[`464d8cf6`](https://github.com/debezium/debezium/commit/464d8cf6) ("DBZ-8786 Improve Error
Handling for Duplicate server_id / server_uuid in MySQL Connector", 2025-03-17). The earlier
code held a separate `Throwable current` for the walk, so `super.isRetriable(throwable)`
received the original exception. The refactor removed `current`.

**First affected release:** `v3.1.0.CR1`. `v3.1.0.Beta1` is clean.

`PostgresErrorHandler` does the same kind of scan correctly — it walks a separate `cause`
variable and leaves the parameter intact.

**Proposed fix:** restore a local cursor, one line changed per connector.

```java
@Override
protected boolean isRetriable(Throwable throwable) {
for (Throwable cause = throwable; cause != null; cause = cause.getCause()) {
if (cause instanceof SQLException sqlException) {
if (NON_RETRIABLE_ERROR_CODES.contains(sqlException.getErrorCode())) {
return false;
}
}
}
return super.isRetriable(throwable);
}
```

The 1236 scan must keep its own loop, because the base class only walks the chain for the
`communicationExceptions()` test.

I have the fix and unit tests for both connectors ready, and will open a PR against this issue.

---

**Do you see the same behaviour using the latest released Debezium version?**

Yes. Verified by reading the source at `v3.5.2.Final`, `v3.6.2.Final`, `v3.7.0.Beta1` and
`main` — all four are identical and carry the defect.

---

**Do you have the connector logs, ideally from start till finish?**

The log line is the standard one:

```
An exception occurred in the change event producer. This connector will be stopped.
```

The message is the tell: for a `java.io.EOFException` the handler should have chosen "will be
restarted".

---

**How to reproduce the issue using our tutorial deployment?**

The defect is visible by inspection, and a unit test is the cheapest reproducer:

```java
assertThat(errorHandler.isRetriable(new IOException("boom"))).isTrue();
```

This fails on `main` and passes with the fix. `PostgresErrorHandlerTest` has the same shape of
test for the Postgres handler.

End-to-end: run the tutorial MySQL connector, then kill the binlog connection server-side (for
example `KILL `, or let `net_write_timeout` expire while the consumer is
slow). The task stops instead of restarting.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.