Skip to content

CAMEL-26339: camel-plc4x - log a run of failed polls once and stop polling a lost connection without autoReconnect - #28556

Merged
davsclaus merged 3 commits into
apache:mainfrom
etzel-mes:fix/CAMEL-26339-plc4x-failed-poll-logging
Oct 27, 2026
Merged

davsclaus merged 3 commits into
apache:mainfrom
etzel-mes:fix/CAMEL-26339-plc4x-failed-poll-logging

Conversation

@etzel-mes

Copy link
Copy Markdown
Contributor

Description

Follow-up of CAMEL-25617 (#28387), JIRA: CAMEL-26339. This implements the first two directions of the ticket.

Failed polls. The triggered consumer still fails every poll that cannot read the PLC, so the consumer health check, bridgeErrorHandler and a custom exceptionHandler see each failure as before. What changes is the consumer's default exception handler, which the consumer now sets itself, as NettyConsumer does with NettyConsumerExceptionHandler:

  • the first failed poll of a run is logged at WARN with the stack trace, as before;
  • the following ones are logged at DEBUG with the message only, as GridFsConsumer does since CAMEL-25711;
  • the first poll that reads the PLC again logs one INFO line with the number of failed polls.

A configured exceptionHandler or bridgeErrorHandler=true still replaces this handler.

autoReconnect=false. Plc4XEndpoint.reconnectIfNeeded() logs "Connection lost and auto-reconnect is turned off, shutting down Plc4XEndpoint" and stops the endpoint once per lost connection, instead of on every call, and not at all when no connection could be set up. So the producer and the polling consumer, which share it, fail their exchanges without repeating the warning.

Once the endpoint has shut the lost connection down, the triggered consumer fails that poll and stops polling with getScheduler().unscheduleTask(), as repeatCount does. That failure is logged at WARN even within a run, and restarting the route starts polling again. Unscheduling from the poll interrupts the polling thread, so the consumer clears that interrupt before the failure is reported.

What PLC4X still logs. With autoReconnect=true, every poll during an outage still tries to connect. PLC4X's TCP transport logs each failed attempt at ERROR with the stack trace and then throws the same failure to Camel (TcpTransportInstance, lines 136–141 in 1.0.0, unchanged on develop). DriverBase also logs an INFO line ("Using connection string") for each attempt. In the live run below, a 10 s outage at period=100 gave 97 of those ERROR lines, each with about 24 stack frames. Camel already reports the failure, so a logger level for org.apache.plc4x.java.transport.tcp.TcpTransportInstance can silence them. I did not find a Camel-side way to handle this that I would prefer, so suggestions are welcome.

Not included: the backoff options (the third direction). They would also thin out the PLC4X lines above, since there would be fewer connect attempts during an outage, at the price of noticing the PLC's return later. But they need new endpoint options. Making Plc4XEndpoint a ScheduledPollEndpoint brings all scheduler options to every plc4x endpoint, including delay next to the existing period. Some of those options do not fit a trigger:

  • backoffIdleThreshold counts every poll without a rising edge as idle, so it thins out the polls and short trigger pulses can be missed;
  • sendEmptyMessageWhenIdle sends an empty exchange on every such poll.

Declaring just backoffErrorThreshold and backoffMultiplier on the endpoint would fit, but I found no other component that does it that way. So I left them out; if you would rather have those two options now, I can add them to this PR.

Verification. Module build only (mvn install -Psourcecheck in components/camel-plc4x; the root build was not run). The module's 43 tests were green in three consecutive runs with -Dsurefire.rerunFailingTestsCount=0. The new route tests fail on the code before the change, except the one that checks that bridgeErrorHandler still gets every failure, which passes before and after. There are no generated changes, and the catalog copy of the component doc is byte-identical.

The live check ran against the snap7 1.4.2 S7 server: a triggered consumer with period=100 ran for 30 s, the server was stopped after 10 s and started again 10 s later, with the camel-plc4x jar from before and after this change:

10 s outage, period=100 before after
autoReconnect=true: "Failed polling endpoint" at WARN with stack trace 95 1
autoReconnect=true: the same at DEBUG, message only – 97
autoReconnect=true: INFO when a poll succeeds again – 1 ("after 98 failed polls")
autoReconnect=true: PLC4X TcpTransportInstance ERROR with stack trace 95 97
autoReconnect=false: "Failed polling endpoint" at WARN 194 1
autoReconnect=false: "Connection lost ... shutting down Plc4XEndpoint" 194 1
autoReconnect=false: failed polls 194, until the run ended 1, then polling stopped

@davsclaus could you have a look? (I cannot request reviewers.)

Target

  • I checked that the commit is targeting the correct branch (Camel 4 uses the main branch)

Tracking

  • If this is a large change, bug fix, or code improvement, I checked there is a JIRA issue filed for the change (usually before you start working on it).

Apache Camel coding standards and style

  • I checked that each commit in the pull request has a meaningful subject line and body.

  • I have run mvn clean install -DskipTests locally from root folder and I have committed all auto-generated changes.

AI-assisted contributions

  • If this PR includes AI-generated code, commits have proper co-authorship attribution (e.g., Co-authored-by trailers) and the PR description identifies the AI tool used.

The code, tests and documentation were written with Claude Code (Claude Opus 5.5) under my direction; every commit carries the Co-Authored-By trailer.

🤖 Generated with Claude Code

Claude Code on behalf of etzel-mes

etzel-mes and others added 2 commits October 27, 2026 11:36
… consumer once

The triggered consumer logged every failed poll at WARN with the stack trace, so a PLC that cannot be
reached logged a stack trace on every poll. Its default exception handler now logs the first failed
poll of a run at WARN with the stack trace and the following ones at DEBUG without it, and the
consumer logs at INFO when a poll succeeds again. A configured exceptionHandler or bridgeErrorHandler
still gets every failure, and the failed polls still count for the health check.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…polling without autoReconnect

Without autoReconnect, every call of reconnectIfNeeded() after a lost connection logged the shutdown
WARN and stopped the endpoint again, while the triggered consumer kept polling. The endpoint now shuts
down once per lost connection, and not when no connection could be set up, so the producer and the
polling consumer fail their exchanges without repeating the WARN. The triggered consumer fails the
poll that finds the connection lost and stops polling until the route is restarted, so its health
check stays down; its default exception handler logs that failure at WARN, even after other failed
polls. Unscheduling from the poll interrupts the polling thread, so that interrupt is cleared before
the failure is reported.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR

Comment — the triggered plc4x consumer now logs a run of failed polls once at WARN (then DEBUG, then one INFO when the PLC is back), and without autoReconnect a lost connection shuts the endpoint down once and stops the polling until the route is restarted. Well scoped, tested, and documented; only non-blocking suggestions below.

Blockers

  • None

Other findings: 3 suggestions (inline), none blocking.

Thanks @etzel-mes for the very thorough description and the before/after numbers from the live snap7 run — that made this easy to review.

What I checked:

  • FailedPollExceptionHandler is set in the constructor, before DefaultEndpoint.configureConsumer() runs, so exceptionHandler=#... and bridgeErrorHandler=true still replace it (and bridgeErrorHandlerRoutesEveryFailedPollToTheErrorHandlerOfTheRoute covers the latter).
  • ScheduledPollConsumer.doRun() calls the handler before errorCounter.incrementAndGet(), so getErrorCounter() == 0 is indeed the first failure of a run, and the counter in poll() still holds the run length before the success resets it.
  • unscheduleTask() cancels the future with cancel(true) on the polling thread itself, so the interrupt is already set when Thread.interrupted() clears it; isSchedulerStarted() is !futures.isEmpty(), so the stopping poll is logged at WARN. A restarted route reschedules.
  • ScheduledPollConsumerHealthCheck reports DOWN while errorCounter > 0, so the consumer stays DOWN after the polling stops, as the description says.
  • The other users of the shared connection (Plc4XConsumer, Plc4XPollingConsumer, Plc4XProducer) go through buildPlc*Request(), which already throws PlcConnectionException on a null connection, so the silent return in reconnectIfNeeded() doesn't introduce an NPE.
  • Docs, catalog copy and the 4.24 upgrade-guide entry (same unreleased version as CAMEL-25617) are updated.

CI has not run yet: the workflows are waiting for a maintainer to approve them.

This review covers project rules and conventions; it does not replace CI, SonarCloud or other review tools.

This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

Claude Code on behalf of davsclaus

…the poll that stops it

The failed poll that stops the polling of the triggered consumer logged the
message of the scheduled poll consumer, which announces a next poll. It now
says that polling stops until the route is restarted. The consumer asks the
endpoint whether it has a connection through hasConnection() instead of
reading the connection field.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

@oscerd oscerd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughtful fix on two fronts — noisy logging and connection-lost handling — for the triggered consumer.

Logging: a FailedPollExceptionHandler logs the first failed poll of a run at WARN with the stack trace and the rest at DEBUG, and a recovered poll at INFO ("succeeded again after N failed polls"), so a PLC that can't be reached no longer WARN-spams every period. The level decision keys off getErrorCounter() == 0 || !isSchedulerStarted() — the first failure of a run (counter still 0, reported before it's counted) and the poll that stops the scheduler get the full WARN, the rest DEBUG — and the "succeeded again" line reads the counter before it's reset. That timing is tightly coupled to ScheduledPollConsumer's internal ordering, but it's clearly commented and the dedicated repeated-failure test locks it in.

Connection-lost: with autoReconnect=false, reconnectIfNeeded() now closes the lost connection under the lock so only one concurrent caller shuts the endpoint down (and returns early when connection == null, nothing left to lose), and read() detects the shut-down connection, unschedules the task so polling stops until the route is restarted, and throws so the poll fails and the health check stays down. Clearing the interrupt (Thread.interrupted()) that unscheduleTask() set, so the failure is reported as a normal poll failure rather than an interrupt, is a subtle but correct touch — and calling stop() outside the lock (per the comment, a concurrent stop() holds the service lock while doStop() waits) avoids the deadlock. The upgrade guide documents the new "lost connection stops polling until restart" behaviour.

No blockers. (CI hasn't run here — fork PR — worth triggering before merge.)

Claude Code on behalf of oscerd

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The three concerns from the earlier review are all addressed in f26f939. The implementation holds up under scrutiny:

  • FailedPollExceptionHandler.handleException(String, Throwable) correctly overrides the 2-arg method that ScheduledPollConsumer.doRun() calls. The handler fires before errorCounter.incrementAndGet(), so getErrorCounter() == 0 reliably identifies the first failure of a run. ✅
  • isSchedulerStarted() reads futures.isEmpty(), and unscheduleTask() cancels futures and clears the list — so the "polling stops" branch in the handler fires correctly on the poll that called unscheduleTask(). ✅
  • hasConnection() reads the volatile field directly; concurrent polls that see a stale true will proceed to a read that throws, then the next poll shuts down — both outcomes are safe. ✅
  • Thread.interrupted() clears the interrupt set by cancel(true) on the current thread; no race possible since it is the same thread. ✅
  • Pre-fix test validation: logsARunOfFailedPollsOnceAtWarnAndItsEndOnceAtInfo asserts the exact level sequence [WARN, DEBUG, DEBUG, INFO, …]; before the fix the default handler logs every failure at WARN, so the assertion would fail. ✅

One low-severity observation: LogCapture sets the Log4j logger level globally (not per-thread). If Maven runs these tests in parallel across the module, two test instances capturing the same logger class simultaneously will interfere. CamelTestSupport creates isolated Camel contexts but shares the JVM logger. This is not blocking — the fix is correct and the tests pass in isolation — but adding @Isolated (JUnit 5) or a note in the Javadoc of LogCapture would prevent intermittent failures if the Maven Surefire fork count is set below the test class count.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@github-actions

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • catalog/camel-catalog
  • components/camel-plc4x
  • docs

🔁 1 test passed only after a retry on JDK 21 (1 retried attempt)

Recovered flaky tests on JDK 21 (1)
Module Test Failed attempts First failure
dsl/camel-jbang/camel-jbang-plugin-tui MonitorStartupTest.browserOnlyServesUntilTheServerStops 1 Connection refused

ℹ️ These tests did not fail the build. Surefire retried them and they passed.
Retries are enabled project-wide by surefire.rerunFailingTestsCount in parent/pom.xml.


🔬 Scalpel shadow comparison — Scalpel: 8 of 708 tested, 25 compile-only — current: 8 all tested

Maveniverse Scalpel detected 8 affected modules (current approach: 8).

Skip-tests mode would test 8 modules (3 direct + 7 downstream), skip tests for 25 (generated code, meta-modules)

Modules Scalpel would test (8)
  • camel-jbang-mcp ← downstream of org.apache.camel:camel-catalog
  • camel-jbang-plugin-mcp ← downstream of org.apache.camel:camel-jbang-core
  • camel-jbang-plugin-tui ← downstream of org.apache.camel:camel-catalog
  • camel-jbang-plugin-validate ← downstream of org.apache.camel:camel-yaml-dsl-validator
  • camel-launcher-container ← downstream of org.apache.camel:camel-launcher
  • camel-plc4x ← components/camel-plc4x/src/main/docs/plc4x-component.adoc, components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XEndpoint.java, components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XTriggeredConsumer.java, components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/LogCapture.java, components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XConsumerRouteTest.java, components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XRepeatedFailureRouteTest.java, components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/PollFailures.java
  • camel-yaml-dsl-validator ← downstream of org.apache.camel:camel-catalog
  • camel-yaml-dsl-validator-maven-plugin ← downstream of org.apache.camel:camel-yaml-dsl-validator
Modules with tests skipped (25)
  • apache-camel
  • camel-allcomponents
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • dummy-component

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

All tested modules (35 modules, 6m 3s total)

Total reactor time: 6m 3s

Module Duration Status
Camel :: Launcher 49.7s SUCCESS
Camel :: JBang :: MCP 42.0s SUCCESS
Camel :: PLC4X 32.2s SUCCESS
Camel :: JBang :: Plugin :: Validate 29.8s SUCCESS
Camel :: YAML DSL :: Validator 25.3s SUCCESS
Camel :: Catalog :: Camel Catalog 23.3s SUCCESS
Camel :: YAML DSL 21.8s SUCCESS
Camel :: Component DSL 18.5s SUCCESS
Camel :: JBang :: Plugin :: Testing 15.7s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 15.2s SUCCESS
Camel :: Docs 14.9s SUCCESS
Camel :: Kamelet Main 12.9s SUCCESS
Camel :: Catalog :: Camel Report Maven Plugin 12.7s SUCCESS
Camel :: YAML DSL :: Deserializers 9.1s SUCCESS
Camel :: Catalog :: Camel Route Parser 8.9s SUCCESS
Camel :: All Components Sync point 5.6s SUCCESS
Camel :: YAML DSL :: Validator Maven Plugin 4.7s SUCCESS
Camel :: YAML DSL :: Maven Plugins 4.0s SUCCESS
Camel :: Catalog :: Maven 2.8s SUCCESS
Camel :: Assembly 2.5s SUCCESS
Camel :: Catalog :: Suggest (deprecated) 2.1s SUCCESS
Camel :: Coverage 1.4s SUCCESS
Camel :: Catalog :: Dummy Component 1.2s SUCCESS
Camel :: JBang :: Plugin :: Edit 1.1s SUCCESS
Camel :: JBang :: Plugin :: MCP 1.1s SUCCESS
Camel :: JBang :: Plugin :: Generate 1.1s SUCCESS
Camel :: Catalog :: Console 1.0s SUCCESS
Camel :: JBang :: Integration tests 0.9s SUCCESS
Camel :: Endpoint DSL :: Support 0.7s SUCCESS
Camel :: JBang :: Main 0.7s SUCCESS
Camel :: Launcher :: Container 0.7s SUCCESS
Camel :: Endpoint DSL n/a
Camel :: Integration Tests n/a
Camel :: JBang :: Core n/a
Camel :: JBang :: Plugin :: TUI n/a

Top 20 slowest modules:

  • Camel :: Launcher (49.7s)
  • Camel :: JBang :: MCP (42.0s)
  • Camel :: PLC4X (32.2s)
  • Camel :: JBang :: Plugin :: Validate (29.8s)
  • Camel :: YAML DSL :: Validator (25.3s)
  • Camel :: Catalog :: Camel Catalog (23.3s)
  • Camel :: YAML DSL (21.8s)
  • Camel :: Component DSL (18.5s)
  • Camel :: JBang :: Plugin :: Testing (15.7s)
  • Camel :: JBang :: Plugin :: Kubernetes (15.2s)
  • Camel :: Docs (14.9s)
  • Camel :: Kamelet Main (12.9s)
  • Camel :: Catalog :: Camel Report Maven Plugin (12.7s)
  • Camel :: YAML DSL :: Deserializers (9.1s)
  • Camel :: Catalog :: Camel Route Parser (8.9s)
  • Camel :: All Components Sync point (5.6s)
  • Camel :: YAML DSL :: Validator Maven Plugin (4.7s)
  • Camel :: YAML DSL :: Maven Plugins (4.0s)
  • Camel :: Catalog :: Maven (2.8s)
  • Camel :: Assembly (2.5s)

⚙️ View full build and test results

@davsclaus davsclaus added this to the 4.24.0 milestone Oct 27, 2026
@davsclaus davsclaus added the enhancement New feature or request label Oct 27, 2026
@davsclaus
davsclaus merged commit b18e452 into apache:main Oct 27, 2026
7 checks passed
davsclaus added a commit that referenced this pull request Oct 27, 2026
LogCapture sets the level of a logger for the whole JVM and MockDriver is
programmed statically, so the test must not run in parallel with other test
classes when JUnit parallel execution is turned on. Follow-up of the review of
#28556.

Signed-off-by: Claus Ibsen <claus.ibsen@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants