[ 
https://issues.apache.org/jira/browse/CAMEL-25654?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Work on CAMEL-25654 started by shashank.
----------------------------------------
> camel-pg-replication-slot - consumer never connects again after the 
> PostgreSQL server closed the connection (restart, shutdown), so the route 
> fails every poll for as long as it runs
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-25654
>                 URL: https://issues.apache.org/jira/browse/CAMEL-25654
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-pg-replicationslot
>            Reporter: shashank
>            Assignee: shashank
>            Priority: Major
>
> The consumer is meant to connect again when its connection to PostgreSQL is 
> lost: {{PgReplicationSlotConsumer.poll()}} (lines at main 1de5ec5135f8) 
> catches the {{SQLException}} of the stream read (117-125) and calls 
> {{connect()}} - but only when {{e.getCause() instanceof SocketException}}. 
> Everything else is rethrown and the connection is kept. {{getStream()}} (92) 
> is called before that {{try}}, so nothing it throws leads to a reconnect.
> h3. 1. A server restart or shutdown is never followed by a reconnect
> When the server is restarted or shut down, it closes the connection of the 
> walsender. The PostgreSQL JDBC driver (42.7.13, read with javap) behaves like 
> this:
> * {{QueryExecutorImpl.readFromCopy}} reports the failed read as 
> {{PSQLException}} "Database connection failed when reading from copy", SQL 
> state {{08006}}, with the {{IOException}} of the socket as cause - an 
> {{EOFException}} when the server closed the connection, not a 
> {{SocketException}}. Before throwing it releases the copy operation, so 
> {{V3PGReplicationStream.isClosed()}} ({{closeFlag || !copyDual.isActive()}}) 
> is true from then on. The consumer rethrows and keeps the connection.
> * The next poll's {{getStream()}} sees the closed stream and runs the slot 
> query ({{isSlotActive}}) on the dead connection: 
> {{QueryExecutorImpl.execute}} fails with "An I/O error occurred while sending 
> to the backend." ({{08006}}) and aborts the connection. This is outside the 
> {{try}}, so it is just rethrown.
> * Every later poll fails in {{getStream()}} with "This connection has been 
> closed." ({{08003}}, {{PgConnection.checkClosed}}).
> The route stays started, the scheduled poll consumer logs a WARN for every 
> poll (every 500 ms with the default {{delay}}), and nothing is consumed 
> again, also long after the server is back. Only a restart of the route helps. 
> A server that ends the replication (the read returns null and the stream is 
> closed) and then closes the connection takes the same {{getStream()}} path.
> h3. 2. A reconnect that fails once is never tried again
> When the cause is a {{SocketException}} (connection reset), {{connect()}} 
> (252-264) closes the old connection first and then opens a new one. If the 
> server is still down - which is normal right after a connection reset - 
> {{newDbConnection()}} throws, and the field keeps the closed connection and 
> its closed stream. Every later poll fails in {{getStream()}} with "This 
> connection has been closed." ({{08003}}, no {{SocketException}}), so again no 
> further reconnect.
> How common: any restart of the PostgreSQL server (minor version upgrade, 
> configuration change that needs a restart, failover, maintenance, 
> {{pg_terminate_backend}} of the walsender) with default options. A consumer 
> of a replication slot typically runs for a long time, so it will see such 
> restarts. It is not silent (a WARN with stack trace per poll), but it never 
> recovers.
> h3. Reproduction
> New {{PgReplicationSlotConsumerReconnectTest}}: the JDBC objects are Mockito 
> mocks that fail the way the driver fails (a read after the server went away: 
> {{08006}} with the socket's exception as cause, and the stream is closed; a 
> query on such a connection: {{08006}} "An I/O error occurred while sending to 
> the backend." and the connection is closed; a closed connection: {{08003}} 
> "This connection has been closed."; the slot sends again what was not 
> flushed). The route uses {{startScheduler=false}} and the test calls 
> {{poll()}} itself, so nothing depends on timing. On main:
> * server stopped (read fails with an {{EOFException}} cause), polled twice, 
> started again, a new message published, polled up to five times: {{the 
> consumer did not consume again after the PostgreSQL server was back; received 
> [first], poll failures [Database connection failed when reading from copy, An 
> I/O error occurred while sending to the backend., This connection has been 
> closed., ...]}};
> * the same with a {{SocketException}} cause, so the first reconnect runs 
> while the server is still down: {{received [first], poll failures [Connection 
> to localhost:5432 refused., This connection has been closed., ...]}};
> * the server ends the replication and closes the connection: {{received 
> [first], poll failures [An I/O error occurred while sending to the backend., 
> This connection has been closed., ...]}};
> * the first delivery of a message fails while the server goes away: 
> {{received [first], poll failures [An I/O error occurred while sending to the 
> backend., This connection has been closed., ...]}}.
> h3. Proposed fix
> * A connection is lost when the SQL state is of class {{08}} (connection 
> exception) or {{57P}} (operator intervention, e.g. {{57P01}} admin shutdown; 
> this only notices the loss one poll earlier), when the cause is a 
> {{SocketException}} (as before), or when the connection is closed. Then the 
> consumer closes the connection (ignoring errors), forgets it, its stream and 
> the payload that was read but not processed successfully, and rethrows.
> * {{poll()}} connects first when there is no connection, and {{getStream()}} 
> is inside the same {{try}}. A connect that fails after the connection was 
> opened closes it again, so the next poll starts from the beginning. The poll 
> delay and the backoff options of the scheduled poll consumer apply as before.
> * The INFO line about the lost connection is logged only when a connection 
> was dropped, not for every failed connect while the server is down.
> * Other errors keep the connection, as before (a test pins that: an error 
> with SQL state {{42704}} does not open a second connection).
> Delivery: the fix relies on the slot resuming at its {{confirmed_flush_lsn}}. 
> The stream is started without a start position and the consumer sets the 
> flushed LSN only after a successful processing, so the server sends a dropped 
> (unflushed) payload again on the new connection. Delivery stays 
> at-least-once, as the component documentation says ("the same message might 
> come more than once"); dropping the payload avoids routing it once from the 
> old connection and once more from the server. A test pins it: a message whose 
> first delivery failed while the connection was lost is delivered exactly once 
> after the reconnect.
> No option, default or output changes, so no upgrade guide entry. While the 
> server is down there is one connect attempt per poll, with the WARN line of 
> the failed poll that main logs already.
> camel-pg-replication-slot tests with the fix: 7 run, 0 failures, 0 skipped. 
> Not run: a real PostgreSQL server (the module's integration test needs a 
> container); which message a restart produces first (ErrorResponse {{57P01}}, 
> end of the replication, or end of the socket) was not observed.
> How it was found: a sweep of the reconnect code of the components, then a 
> small TLA+ model of the consumer's connection field (server up / down, 
> connection ok / broken / closed / none, cause of the failed read). For main 
> it reaches a lost connection that no poll replaces, for both causes, and a 
> run that never reconnects; with the fix the properties hold for two outages 
> (126 states). The model is a sketch; the tests are the evidence.
> Affected: main (1de5ec5135f8; tested). camel-4.23.x, camel-4.22.x, 
> camel-4.18.x and camel-4.14.x have the same {{SocketException}} check in 
> {{poll()}} and the same {{connect()}} (read, not run).
> Duplicate check (2026-10-14): JIRA component camel-pg-replicationslot (only 
> CAMEL-15614, automate the integration test), text "replication slot 
> reconnect" (nothing); GitHub pull requests "PgReplicationSlotConsumer", 
> "pg-replication-slot" (nothing related).
> _Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to