MySQL/MariaDB error handlers pass a null throwable to super.isRetriable(), so every error is non-retriable
- 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
Assessment
This issue has not been assessed yet.