Repository navigation
CAMEL-25617: camel-plc4x - Upgrade to PLC4X 1.0.0 - #28387
Conversation
|
🌟 Thank you for your contribution to the Apache Camel project! 🌟 🐫 Apache Camel Committers, please review the following items:
|
gnodet-bot
left a comment
There was a problem hiding this comment.
LGTM. The scraper-based triggered consumer is cleanly replaced by a ScheduledPollConsumer implementation that correctly handles the rising-edge semantics, polls trigger and data tags in a single request, and properly manages connection lifecycle. The new Plc4XConsumerRouteTest covers the critical paths at the route seam (rising edge, held trigger, stop/restart without duplicates, failure recovery, reconnection, validation). The endpoint option stripping from the connection string is well-guarded and tested. No production-impacting issues found.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
|
🧪 CI tested the following changed modules:
✅ POM dependency changes: targeted tests included Changed properties: plc4x-version Modules affected by dependency changes (1)
🔁 1 test passed only after a retry on JDK 21 (1 retried attempt) Recovered flaky tests on JDK 21 (1)
🔬 Scalpel shadow comparison — Scalpel: 9 of 707 tested, 3 compile-only — current: 8 all testedMaveniverse Scalpel detected 9 affected modules (current approach: 8). Changed properties: plc4x-version Skip-tests mode would test 9 modules (4 direct + 0 downstream), skip tests for 3 (generated code, meta-modules)
|
| Module | Duration | Status |
|---|---|---|
| Camel :: Launcher | 59.2s | SUCCESS |
| Camel :: JPA | 54.5s | SUCCESS |
| Camel :: JBang :: MCP | 49.3s | SUCCESS |
| Camel :: PLC4X | 35.7s | SUCCESS |
| Camel :: YAML DSL :: Validator | 30.6s | SUCCESS |
| Camel :: JBang :: Plugin :: Validate | 30.3s | SUCCESS |
| Camel :: Catalog :: Camel Catalog | 25.8s | SUCCESS |
| Camel :: YAML DSL | 23.1s | SUCCESS |
| Camel :: Component DSL | 22.5s | SUCCESS |
| Camel :: JBang :: Plugin :: Testing | 21.4s | SUCCESS |
| Camel :: Kamelet Main | 19.6s | SUCCESS |
| Camel :: JBang :: Plugin :: Kubernetes | 18.1s | SUCCESS |
| Camel :: Docs | 16.4s | SUCCESS |
| Camel :: YAML DSL :: Deserializers | 9.3s | SUCCESS |
| Camel :: Catalog :: Camel Report Maven Plugin | 8.3s | SUCCESS |
| Camel :: Catalog :: Camel Route Parser | 8.3s | SUCCESS |
| Camel :: Assembly | 5.7s | SUCCESS |
| Camel :: All Components Sync point | 5.4s | SUCCESS |
| Camel :: YAML DSL :: Validator Maven Plugin | 4.0s | SUCCESS |
| Camel :: Catalog :: Maven | 4.0s | SUCCESS |
| Camel :: Coverage | 3.3s | SUCCESS |
| Camel :: YAML DSL :: Maven Plugins | 3.3s | SUCCESS |
| Camel :: Catalog :: Suggest (deprecated) | 3.1s | SUCCESS |
| Camel :: JBang :: Plugin :: Edit | 1.4s | SUCCESS |
| Camel :: JBang :: Plugin :: MCP | 1.3s | SUCCESS |
| Camel :: JBang :: Integration tests | 1.2s | SUCCESS |
| Camel :: JBang :: Plugin :: Generate | 1.1s | SUCCESS |
| Camel :: Catalog :: Dummy Component | 1.1s | SUCCESS |
| Camel :: Catalog :: Console | 1.1s | SUCCESS |
| Camel :: JBang :: Main | 1.0s | SUCCESS |
| Camel :: Endpoint DSL :: Support | 0.8s | SUCCESS |
| Camel :: Launcher :: Container | 0.5s | 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(59.2s)Camel :: JPA(54.5s)Camel :: JBang :: MCP(49.3s)Camel :: PLC4X(35.7s)Camel :: YAML DSL :: Validator(30.6s)Camel :: JBang :: Plugin :: Validate(30.3s)Camel :: Catalog :: Camel Catalog(25.8s)Camel :: YAML DSL(23.1s)Camel :: Component DSL(22.5s)Camel :: JBang :: Plugin :: Testing(21.4s)Camel :: Kamelet Main(19.6s)Camel :: JBang :: Plugin :: Kubernetes(18.1s)Camel :: Docs(16.4s)Camel :: YAML DSL :: Deserializers(9.3s)Camel :: Catalog :: Camel Report Maven Plugin(8.3s)Camel :: Catalog :: Camel Route Parser(8.3s)Camel :: Assembly(5.7s)Camel :: All Components Sync point(5.4s)Camel :: YAML DSL :: Validator Maven Plugin(4.0s)Camel :: Catalog :: Maven(4.0s)
davsclaus
left a comment
There was a problem hiding this comment.
TL;DR
Comment: the port to PLC4X 1.0.0 is clean and well tested, and the existing upgrade guide notes are right. Two 1.0.0 changes that break user routes are missing from the upgrade guide.
Blockers
- None in the code. I would like the upgrade guide (
camel-4x-upgrade-guide-4_24.adoc, near line 2651) to cover these before merge:- Tag addresses: as far as I can see, 1.0.0 writes array selection as a range before the data type (
holding-register:1:INT[4]→holding-register:1[0..3]:INT,%DB42:28.0:BYTE[4]→%DB42:28.0[0..3]:BYTE), and the old form no longer parses. That affects thetag.*options,triggerand the producer body. Please check and add a sentence with an example. s7-lightis gone:plc4j-driver-s70.13.1 registeredS7DriverandS7LightDriver, and 1.0.0 registers onlyS7Driver. Soplc4x:s7-light://stops working, and the PLC4X release notes don't say so.
- Tag addresses: as far as I can see, 1.0.0 writes array selection as a range before the data type (
Other findings: 3 non-blocking, 2 suggestions. See below.
Non-blocking
Plc4XTriggeredConsumer.java:88:Boolean.TRUE.equals(response.getObject(TRIGGER))is false for an INT that reads 1, or for an array, which 1.0.0 now returns on OPC UA/ADS for an address with no selection. The consumer then polls forever without delivering or logging anything. Failing the poll when the value is not aBooleanwould make that visible.Plc4XEndpoint.java:155:reconnectIfNeeded()now closes and replaces the connection, and the producer calls it for every exchange without a lock. Two senders can each replace it, and one connection leaks or gets closed under the other's write. MakereconnectIfNeeded()/setupConnection()synchronizedand the fieldvolatile.- With
autoReconnect=false, after a lost connection the triggered consumer logs the "auto-reconnect is turned off" WARN plus a stack trace on every poll (Plc4XEndpoint.java:165/322,Plc4XTriggeredConsumer.java:116). Before, the scraper just went quiet. Logging once, or stopping polling, could be a follow-up.
Suggestions
Plc4XTriggeredConsumer.java:122:.execute().get()has no timeout, unlike the producer'sget(5000, ms).- The note at line 2660 that the route fails to start could add that this fails the CamelContext start too, unless the route is supervised or
autoStartup=false. - 1.0.0 brings
jackson-dataformat-xmlin viaplc4j-tools-event-pump. Could you paste a before/aftermvn dependency:treeto show the exclusions still hold?
CI: "PR doc validation" fails on cbor-dataformat.adoc:63. That is a problem on main, not this PR.
Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
|
Closed and reopened only to get a new CI run: "PR doc validation" failed on Claude Code on behalf of davsclaus |
a59fa64 to
a19f39b
Compare
|
Thanks @davsclaus, all points checked against the 0.13.1 and 1.0.0 jars. Pushed 98c1acd, d5eef55 and bd9fbb9. Upgrade guide (bd9fbb9):
Non-blocking / suggestions:
Dependency tree,
So the exclusions still hold and no Jackson artifact is in compile scope any more. The module no longer declares event-pump; it comes in at runtime through the drivers' subscription emulation, which only uses Module build with tests and Claude Code on behalf of etzel-mes |
gnodet-bot
left a comment
There was a problem hiding this comment.
Following up on my earlier review — all previous issues and davsclaus's blockers are addressed in the 3 new commits.
The three commits cover: tag address format and s7-light removal in the upgrade guide, non-boolean trigger detection (fails the poll with a PlcException naming the address and value type), a 5 s timeout on the triggered read via the shared REQUEST_TIMEOUT_MILLIS constant, and the connection lifecycle refactor (volatile field, synchronized blocks in setupConnection/reconnectIfNeeded/doStop, closeLostConnection, stop() called outside the lock). All of davsclaus's blockers and non-blocking items are addressed.
One observation, not a blocker: Plc4XTriggeredConsumer.read() checks plc4XEndpoint.connection == null and then calls plc4XEndpoint.buildPlcReadRequest(...) (which dereferences connection) without holding the endpoint lock between the two. A concurrent doStop() that nulls connection under the lock could slip in between them and cause an NPE on connection.readRequestBuilder(). This is the same pattern carried by Plc4XConsumer, Plc4XPollingConsumer, and Plc4XProducer today, so it is component-wide and not introduced by this PR. In practice ScheduledPollConsumer runs the poll to completion before the executor shuts down during stop, so the window is effectively closed at runtime. Worth a follow-up cleanup, but not a reason to hold this PR.
The 14 route-level tests in Plc4XConsumerRouteTest cover the contract thoroughly: rising edge detection, data delivery without the trigger key in the body, stop/restart without duplicates, error recovery (failed reads, non-boolean trigger, error response codes, refused connections), reconnection after dropped connections, and driver parameter stripping. The upgrade guide section is accurate and complete.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
|
Following up on the real hardware remark on CAMEL-25617: results for bd9fbb9 on a real PLC. Setup: a Siemens S7-1200 (CPU 1214C, 6ES7 214-1AG40-0XB0) on a customer test line, reached from a Windows Server 2025 machine on the plant network. The connection string was
Things I saw that are outside this PR, for separate tickets if you think they are worth it:
|
davsclaus
left a comment
There was a problem hiding this comment.
TL;DR
Approve: thanks for the three follow-up commits and for testing on a real S7-1200. Every point of my earlier review is handled, and the upgrade guide now covers the two route-breaking changes of PLC4X 1.0.0.
Blockers
- None
Other findings: 2 non-blocking follow-ups, which you already named yourself. See below.
Earlier points
| Earlier point | Status |
|---|---|
Upgrade guide: array notation of tag addresses (holding-register:1:INT[4] → holding-register:1[0..3]:INT) |
Done (camel-4x-upgrade-guide-4_24.adoc:2678), incl. that [4] before the type now means one element |
Upgrade guide: s7-light driver is gone |
Done (camel-4x-upgrade-guide-4_24.adoc:2684) |
| Non-boolean trigger polled forever without a sign | Done: Plc4XTriggeredConsumer.read() fails the poll with the address and the type, new test aTriggerThatIsNotABooleanFailsThePoll |
reconnectIfNeeded() replaced the shared connection without a lock |
Done: volatile field, replace/setup/close under the endpoint's lock, and stop() outside it, so it cannot deadlock with a concurrent stop() |
.execute().get() without a timeout |
Done: REQUEST_TIMEOUT_MILLIS shared with the producer, the future is cancelled on timeout |
| Failed start also fails the CamelContext start | Done, with a link to the supervising route controller |
Jackson exclusions after plc4j-tools-event-pump |
Done: the before/after dependency tree in the PR shows jackson-databind and woodstox-core still excluded, nothing Jackson in compile scope |
Repeated WARN with autoReconnect=false |
Left for a follow-up, as agreed |
CI on this head: camel-plc4x passes (36 tests) and the generated files are in sync. "Build and test" fails only on SourceSizeTest in camel-jbang-plugin-tui (SourceTab.java has 2007 lines). That was a problem on main, fixed by #28419 after this run. A re-run keeps the old merge commit, so the branch needs an update from main to go green. GitHub cannot update it automatically because it now conflicts with main, so could you rebase it onto main?
Non-blocking (follow-ups)
Plc4XEndpoint.setupConnection()(line 132) still replacesconnectionwithout closing a previous one. This PR adds a new caller: the triggered consumer calls it indoStart(). So a route stop/start, or a producer and a triggered consumer on the same endpoint, leaves one open PLC connection behind (you saw this too). Now that the method holds the lock, a guard there would be a small change: keep a connection that is still connected, and close a lost one first (closeLostConnection()).Plc4XEndpoint.doStop()(line 335) closes the connection onlyif (isConnected()), so a lost connection is never closed or cleared on stop.closeLostConnection()could do this too. This was already the case before the PR.
The open gnodet-bot thread on Plc4XTriggeredConsumer.java:119 (the null check and the use of connection are not one step) is a pattern the whole component already has, so it fits the same follow-up.
This review checks the Camel project conventions. It does not replace static analysis or dedicated review tools.
Reviewed with Claude Code on behalf of Claus Ibsen (@davsclaus). This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.
davsclaus
left a comment
There was a problem hiding this comment.
Thanks @etzel-mes. The red build (21, false) is not caused by this PR. It is SourceSizeTest.noFileOrMethodGrowsOverTheLimits in camel-jbang-plugin-tui (SourceTab.java has 2007 lines), which was broken on main at the time of the run and was fixed by #28419. The branch now conflicts with main. Could you rebase it onto main, so CI runs green before merge? My approval stands.
Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.
bd9fbb9 to
d5b7005
Compare
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after the latest commits (d5b7005).
All three previously flagged concurrency issues remain present. The PR otherwise looks good — version upgrade is clean, the synchronization work is a real improvement over the original, and no new issues were introduced.
Status of prior findings:
setupConnection()— assignsconnectionwithout closing the prior lost connection first (follow-up from davsclaus). Accepted as non-blocking.doStop()— a dropped-transport connection (non-null butisConnected() == false) is never closed or nulled. Accepted as non-blocking.- TOCTOU in
Plc4XTriggeredConsumer.read()—plc4XEndpoint.connectionread outside any lock.
All three fit the same follow-up cleanup. The PR is mergeable as-is once davsclaus's requested rebase is done.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after the latest commits (d5b7005).
All three previously flagged concurrency issues remain present. The PR otherwise looks good — version upgrade is clean, the synchronization work is a real improvement over the original, and no new issues were introduced.
Status of prior findings:
setupConnection()— assignsconnectionwithout closing the prior lost connection first (follow-up from davsclaus). Accepted as non-blocking.doStop()— a dropped-transport connection (non-null butisConnected() == false) is never closed or nulled. Accepted as non-blocking.- TOCTOU in
Plc4XTriggeredConsumer.read()—plc4XEndpoint.connectionread outside any lock.
All three fit the same follow-up cleanup. The PR is mergeable as-is once davsclaus's requested rebase is done.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
davsclaus
left a comment
There was a problem hiding this comment.
Thanks for the rebase, @etzel-mes. CI is green now (camel-plc4x: 36 tests), and the range-diff against the series I looked at last time shows only context changes from main, so everything from the earlier rounds still holds.
I'd like to take back one thing I said last time. I called the two connection leaks follow-ups, but this PR already rewrites both of those spots, and each fix is a couple of lines. Doing them here saves another round on the same lines later. Also, I was wrong that the triggered consumer is a new caller of setupConnection(): on main, Plc4XConsumer.doStart() already called it for the triggered path. So the leak is older than this PR, but it is still on lines the PR changes.
Plc4XEndpoint.setupConnection()(around line 147) overwritesconnectionwithout closing the old one. Endpoints are not stopped when a route stops, so every route stop/start of a plc4x consumer, and a producer and consumer on the same endpoint, leaves an open PLC connection behind, and S7 connection slots are limited. Inside the lock, could we keep a connection that is still connected and replace only a lost one?Thesynchronized (this) { if (!isConnected()) { closeLostConnection(); connection = plcDriverManager.getConnection(connectionString); } }
isConnected()guard matters: always closing first would close a live connection that the producer or another consumer is in the middle of using. Asserting thatMockDriver.connectionStrings()has size 1 after the restart instopsReadingWhenTheRouteStopsAndResumesWithoutDuplicatesAfterARestartwould lock this in.Plc4XEndpoint.doStop()(around line 349) closes onlyif (isConnected()), so a dropped connection is never closed or cleared. CallingcloseLostConnection()there instead would cover both cases, and it also keeps a failingclose()on a dead transport from failing the stop.- Optional, while you're in there:
Plc4XTriggeredConsumer.read()(lines 119-123) checksplc4XEndpoint.connectionfor null and thenbuildPlcReadRequest()reads the field again, so a concurrent close in between would surface as an NPE instead of "Cannot establish connection". Taking a snapshot inbuildPlcReadRequest(Map)and throwing there when it is null would fix it for all four callers.
With those, the open gnodet-bot threads are covered too.
Reviewed with Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.
|
Thanks @davsclaus, all three are in 712fa4a.
Module build with tests and Claude Code on behalf of etzel-mes |
… route tests The test MockDriver can now be programmed per tag address with the values successive reads return (the last value repeats), and can refuse connections. Requests whose tags are not programmed keep the previous deep stub behaviour, so the existing component, tag and pollEnrich tests are unchanged. The driver prepares every tag as a boolean tag, which the scraper needs to validate the trigger. Plc4XConsumerRouteTest drives plc4x:mock: routes and asserts only what arrives at mock endpoints: - one exchange per rising edge of the trigger (false, false, true, true, false, true gives exactly two); - the body is a map of the data tags, without the trigger; - a trigger that is already true on the first read emits once; - a stopped route delivers nothing, and a restarted route delivers again without duplicates; - a consumer without a trigger reads the tags once; - refused connections neither fail the route start nor log a warning per period. These tests replace Plc4XConsumerTest and Plc4XConsumerStopTest, which mocked the endpoint and captured the scraper's constructors with Mockito. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…red consumer to ScheduledPollConsumer PLC4X 1.0.0 no longer publishes plc4j-scraper, which the triggered consumer was built on; no artifact replaces it. The ten driver artifacts keep their names, and the driver SPI (PlcDriver, DefaultPlcDriverManager) stays in plc4j-api. With a trigger set, the endpoint now creates Plc4XTriggeredConsumer, a ScheduledPollConsumer with a fixed delay of period milliseconds and the first poll at start. Each poll reads the trigger tag and the tags in one request on the endpoint's connection, after reconnectIfNeeded(), and emits the tags once when the trigger goes from false or unknown to true. The data therefore comes from the same read that saw the edge, where the scraper read the tags in a second request. A failed poll (connection, read, or a tag answered with an error code) goes to the consumer's exception handler, forgets the trigger value so a trigger that is still true emits again, and polling continues. A read that completes after the consumer stopped is not processed. A trigger without a positive period, or a tag named like the reserved trigger name TRIGGER_VAR, fails the route start; so does a connection that cannot be set up at start without autoReconnect, since no poll would reconnect it. No endpoint option is added or changed: the endpoint stays a DefaultEndpoint, so Camel's scheduler options do not leak into it. The untriggered consumer keeps its one-shot read and loses the scraper code; it and the polling consumer share the read response conversion. The scraper-only Constants.PLC_NAME is removed, and so is the MockDriver subscriber stub, which used SPI classes that moved in 1.0.0. MockDriver can now fail a whole read or answer a tag with an error code for one cycle, accept connections again after refusing them, and hold a read until the test releases it. New route tests cover the data of the edge cycle, the trigger reset after a failed read, an error code on a data tag, a read that completes while the route stops, a trigger without a period, the reserved trigger name and a refused connection at start without autoReconnect. The refused-connection test now asserts the ScheduledPollConsumer contract: the route starts, each failed poll reaches the exception handler, and delivery resumes once the PLC accepts connections, instead of capping the number of warnings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ring parameters The endpoint passed its whole URI, Camel options included, to the PLC4X driver manager as the connection string. PLC4X 1.0.0 drivers report every parameter they do not know as a warning, so each connection logged one warning per Camel option (trigger, period, tag.*, autoReconnect, exceptionHandler, ...). On start the endpoint now removes its own options from the connection string, taking the option names and multi-value prefixes from the component's generated EndpointUriFactory, and keeps every other parameter as it is in the endpoint URI, percent-encoding included. The endpoint keeps reporting its configured URI; the connection string for the driver is kept separately. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…olled trigger Describe triggered reading in the component documentation: the trigger tag is polled every period on the endpoint's connection, the tags are read in the same request and delivered once per rising edge, and a failed poll goes to the consumer's exception handler while polling continues. It states that recovery after a lost connection needs autoReconnect=true, how the start behaves when the PLC cannot be reached, and the reserved trigger name. Add a camel-plc4x section to the 4.24 upgrade guide for PLC4X 1.0.0, its connection-string changes and the changed triggered consumer, and update the catalog copy of the component documentation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…necting it
With autoReconnect the endpoint called connect() on a connection that
reported itself not connected and only took a new connection when that
call returned without one. The PLC4X 1.0.0 S7 and EtherNet/IP connections
cannot connect again once their transport closed: connect() throws
("Transport is not open"), so after a PLC restart every poll of the
triggered consumer, and every write of the producer, failed until the
route was restarted. Run against a snap7 S7 server and libplctag's
ab_server; the Modbus TCP connection happened to reconnect.
The endpoint now closes the lost connection and asks the driver manager
for a new one, which every driver supports.
MockDriver connections now report that they are connected from connect()
until the test drops them, so the route tests exercise the endpoint's one
connection; a new test checks that the triggered consumer polls on a
single connection.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The producer calls reconnectIfNeeded() for every exchange and the consumers for every read, so two callers could each close and replace the connection of the endpoint, leaking one connection or closing it under the other's request. The connection is now replaced, set up and closed while holding the endpoint's lock, and the field is volatile for the readers. stop() is called outside the lock, as a concurrent stop() holds the service lock while doStop() waits for the endpoint's lock. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…gger or no answer A trigger that reads as a number or an array (PLC4X 1.0.0 answers an OPC UA or ADS array address without a selection with an array) was taken as false, so the consumer polled forever without delivering or logging anything. Such a poll now fails with the trigger address and the type that was read, as does a poll with no trigger value. The read of the trigger now waits at most as long as the producer waits for a write, and a read without an answer is cancelled and fails the poll. A failed poll forgets the trigger value and goes to the exception handler of the consumer. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…a failed start The upgrade guide now names the PLC4X 1.0.0 array notation of tag addresses, the removal of the s7-light driver, that a triggered route that cannot connect on start also fails the start of the CamelContext unless it is supervised or not started automatically, and that a non-boolean trigger fails the poll. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…e on stop Every start of a consumer or producer set up a new connection and overwrote the field without closing the old one. An endpoint is not stopped with its route, so each route restart, and a producer and a consumer on the same endpoint, left an open PLC connection behind. A start now keeps a connection that is still connected and closes and replaces only a lost one. Stopping the endpoint closed only a connected connection, so a dropped one was never closed or cleared; it now closes and clears either, and a failing close no longer fails the stop. Building a read request now fails with "Cannot establish connection" when there is no connection, instead of a NullPointerException when the connection is closed between the check of a consumer and the read. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
712fa4a to
afa62bc
Compare
gnodet-bot
left a comment
There was a problem hiding this comment.
Following up on my earlier review — all three points from davsclaus's CHANGES_REQUESTED are addressed.
The latest commit covers exactly what was asked:
-
setupConnection()guard — thesynchronized (this)block keeps a live connection and callscloseConnection()before replacing a lost one.stopsReadingWhenTheRouteStopsAndResumesWithoutDuplicatesAfterARestartassertsconnectionStrings().size() == 1after restart, locking in the behaviour. -
doStop()closes lost connections — now callscloseConnection()undersynchronized (this)unconditionally.closeConnection()triesconnection.close(), swallows any exception from a dead transport, and setsconnection = nullregardless.doStopClosesALostConnectionverifies this with a mock that throws onclose(). -
TOCTOU in
buildPlcReadRequest— the field is read once into a localcurrent; a nullcurrentthrowsPlcConnectionException("Cannot establish connection")before touching anything; all subsequent accesses usecurrent. The inline null checks in the four consumer/producer callers are correctly removed.buildPlcReadRequestWithoutAConnectioncovers the null path.
canWrite() and isConnected() also take local snapshots now, which is correct. The stop() call in reconnectIfNeeded() is deliberately outside the lock to avoid a deadlock with a concurrent stop() that holds the service lock while doStop() waits for the endpoint lock — the comment explains this.
LGTM.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
buildPlcWriteRequest() missing the local-snapshot pattern
Apologies for the oversight in my earlier LGTM — I missed this one.
The PR correctly fixed buildPlcReadRequest(), canWrite(), and isConnected() to read connection into a local variable before use:
PlcConnection current = connection;
if (current == null) { throw new PlcConnectionException(...); }But buildPlcWriteRequest() (line 297 of Plc4XEndpoint.java) was not updated and still dereferences the volatile field directly:
public PlcWriteRequest buildPlcWriteRequest(Map<String, Map<String, Object>> tags) {
PlcWriteRequest.Builder builder = connection.writeRequestBuilder(); // ← data racePlc4XProducer.process() calls reconnectIfNeeded() (synchronized), then isConnected()/canWrite() (both safe with local snapshots), and then buildPlcWriteRequest() — but between the canWrite() check and the writeRequestBuilder() call, a concurrent doStop() can null connection under its own synchronized(this) block, causing an NPE.
Suggested fix (same pattern as buildPlcReadRequest):
public PlcWriteRequest buildPlcWriteRequest(Map<String, Map<String, Object>> tags) throws PlcConnectionException {
PlcConnection current = connection;
if (current == null) {
throw new PlcConnectionException("Cannot establish connection");
}
PlcWriteRequest.Builder builder = current.writeRequestBuilder();
// ... rest unchangedℹ️ Minor note (pre-existing, non-blocking): getTags() returns the internal Map<String, String> directly, making it mutable from the outside. The new Plc4XTriggeredConsumer.doStart() only calls containsKey() on it, so there is no active bug — but returning Collections.unmodifiableMap(tags) or Map.copyOf(tags) would make the API contract explicit.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
…of a NullPointerException Building a write request read the connection field without a check, so a write after the endpoint was stopped, or while its connection was being closed, failed the exchange with a NullPointerException. It now reads the field once and fails with "Cannot establish connection" when there is no connection, as building a read request does. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
@gnodet thanks, the write path is fixed in a214ef9: I left Module build with tests and Claude Code on behalf of etzel-mes |
gnodet-bot
left a comment
There was a problem hiding this comment.
Following up on my earlier review — the buildPlcWriteRequest() finding is now fixed.
The new commit (a214ef9) correctly applies the local-snapshot pattern to buildPlcWriteRequest(): PlcConnection current = connection is read once, a null check throws PlcConnectionException, and current.writeRequestBuilder() is used for the builder — matching what buildPlcReadRequest() already did. The new unit test buildPlcWriteRequestWithoutAConnection directly validates the null-connection path.
Two review threads opened in my earlier re-review (setupConnection() connection leak) target code that was already fixed in the preceding commits (the synchronized (this) { closeConnection(); connection = ... } pattern is present in setupConnection() in the current diff). They appear non-outdated because the file was subsequently touched by this commit, but the concern is addressed.
LGTM.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Following up on my earlier review — all previously flagged issues are now fully addressed.
buildPlcWriteRequest() missing local-snapshot pattern (flagged in my last comment) — ✅ Fixed in the latest commit (a214ef9ebc): reads connection into a local variable, null-checks it, and builds the write request on the snapshot. The companion test buildPlcWriteRequestWithoutAConnection confirms the PlcConnectionException path.
Three concurrency issues from the prior CHANGES_REQUESTED (now resolved across the previous commits):
setupConnection()connection leak — ✅ Fixed:synchronized(this)block callscloseConnection()before obtaining a new connection.stopsReadingWhenTheRouteStopsAndResumesWithoutDuplicatesAfterARestartasserts a single connection string after restart.doStop()drops a disconnected connection’s OS resources — ✅ Fixed:synchronized(this) { closeConnection(); }now runs unconditionally, andcloseConnection()swallows transport-level close failures. Test renamed todoStopClosesALostConnectionand verifiesclose()is called even whenisConnected()is false.- TOCTOU on
Plc4XTriggeredConsumer:reconnectIfNeeded()→buildPlcReadRequest()gap — ✅ Addressed:connectionisvolatile, andbuildPlcReadRequest(additionalTags)reads it into a local snapshot before the null-check and builder call. The volatile guarantees visibility of the post-reconnect write; the local snapshot prevents use-after-null if a concurrentdoStop()clears the field after the snapshot.
The PR is clean. LGTM.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
davsclaus
left a comment
There was a problem hiding this comment.
TL;DR
Approve: the two connection leaks and the null-check race are fixed on the lines this PR rewrites, with tests. This replaces my "changes requested".
Blockers
- None
Other findings: none new.
Thanks @etzel-mes, and thanks for taking these in this PR rather than a follow-up. Re-checked at a214ef9. The range-diff against d5b7005 shows the first eight commits unchanged apart from one context line in the upgrade-guide commit (the rebase), plus the two new commits.
| Point | Status |
|---|---|
setupConnection() overwrote a connection without closing it |
Done: inside the lock it keeps a connection that is still connected and closes and replaces only a lost one. stopsReadingWhenTheRouteStopsAndResumesWithoutDuplicatesAfterARestart asserts a single connection after the restart |
doStop() left a dropped connection open |
Done: closeConnection() always closes and clears it, and a failing close() no longer fails the stop (doStopClosesALostConnection) |
Null check and use of connection were two steps |
Done: buildPlcReadRequest(Map) and, after gnodet-bot's catch, buildPlcWriteRequest(Map) read the field once and throw PlcConnectionException when it is null. The duplicate checks in the three consumers are gone, and both paths have a test |
A note, no action needed: buildPlcReadRequest() and buildPlcWriteRequest(Map) are public and now declare a checked PlcConnectionException. Only camel-plc4x calls them, and the change replaces an NPE with a clear error, so that's fine.
Still open from your own list, for separate tickets if you want them: the WARN with a stack trace on every failed poll, and the PLC4X 1.0.0 S7 BOOL array decoding error (an upstream PLC4X issue).
This was a static re-review; it doesn't replace CI or static analysis.
Reviewed with Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
|
@etzel-mes, about the S7 BOOL array decoding error you found while testing ( The repeated WARN with Claude Code on behalf of davsclaus |
Description
Upgrades
camel-plc4xfrom PLC4X 0.13.1 to 1.0.0 (JIRA: CAMEL-25617). PLC4X 1.0.0 requires Java 21, which Camel main has since CAMEL-22996, and it removed theplc4j-scrapermodule the triggered consumer was built on.The user-facing contract is unchanged: the same endpoint options (
trigger,period,tags,autoReconnect), the sameMap<String,Object>body, the same one-shot consumer, polling consumer and producer. Nothing was added to or removed from the generated endpoint metadata.Triggered consumer. With the scraper gone, the consumer with
triggeris now a CamelScheduledPollConsumeron the endpoint's single connection:This keeps the scraper's semantics (rising edge only, a trigger already true on the first read emits once, a held trigger never re-emits) and changes what was broken or implicit: the data tags come from the same read as the edge (the scraper read them in a later separate request on another connection), a failed read no longer stops triggering for the life of the route (the scraper's collector died on the first failure), stop cancels the schedule, and no scraper thread pools remain (each scraper start leaked up to 20 non-daemon threads). The 0.13.1 behaviour was locked by route tests first (commit 1) and the port is proven by the same tests passing (commits 2 and 5); the two Mockito tests that captured the scraper's constructors, including the stop test from CAMEL-25292, are re-expressed at the route seam.
PLC4X 1.0.0 offers
plc4j-tools-event-pumpin place of the scraper; it is not used here because its cached read request stays bound to the connection that built it, so it never recovers from a PLC restart (apache/plc4x#2832, found while preparing this change). Camel's own polling consumer gives the scheduling, overlap guarantee, stop and error routing without a new dependency.Other changes.
reconnectIfNeeded()now closes a lost connection and takes a new one from the driver manager instead of callingconnect()on it; the 1.0.0 S7 and EtherNet/IP connections refuse to connect again once their transport closed, so without this neither the producer nor the consumers recovered after a PLC restart. The endpoint no longer passes its own Camel options (trigger,period,tag.*,autoReconnect, generic consumer options) to the driver inside the connection string; 1.0.0 drivers log a warning per unknown parameter.Constants.PLC_NAME(scraper-only) is removed. The upgrade guide gets a camel-plc4x section with the PLC4X-side changes (Java 21, renamed connection parameters, OPC UA secure defaults, with a link to the PLC4X release notes) and the behaviour changes above.Not changed, for the record. With the default
autoReconnect=falsethe endpoint stops itself on the first lost connection while the route keeps polling; that is pre-existing and shared with the producer.plc4j-tools-event-pumpstill arrives transitively throughplc4j-utils-subscription-emulation(ab-eth, eip, firmata) with the module's long-standing jackson/woodstox exclusions applying under it; the EtherNet/IP path was run with it.Verification. Module build only (
mvn installincomponents/camel-plc4x; the root build was not run). Module suite: 35 tests, green in three consecutive runs with-Dsurefire.rerunFailingTestsCount=0;-Psourcecheckclean; the catalog copy of the component doc is byte-identical. Live, against protocol simulators with writable memory (S7: snap7 1.4.2 server; Modbus TCP:oitc/modbus-server; EtherNet/IP: libplctag 2.7.2ab_serveras ControlLogix), a producer route wrote the trigger bit and the data tags while a triggered consumer polled atperiod=100: on all three, a false→true edge delivers exactly one exchange whose body holds the values written just before the edge, a trigger held for 2.5 s delivers no second exchange, 20 edges at 1 Hz delivered 20 matching bodies, poll spacing was 105–109 ms mean (max 132 ms), the JVM used about 2 % CPU, a 10 s simulator stop logged one warning per poll and the consumer recovered 0.1–0.6 s after the simulator came back; the unknown-parameter warnings are gone. Not verified against real PLCs.Backport. Not backportable to 4.22.x: PLC4X 1.0.0 requires Java 21.
@davsclaus @oscerd could you have a look? (I cannot request reviewers.) JIRA user: mardonius.
Target
mainbranch)Tracking
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 -DskipTestslocally from root folder and I have committed all auto-generated changes.AI-assisted contributions
Co-authored-bytrailers) and the PR description identifies the AI tool used.The code, tests and documentation were written with Claude Code (Claude Fable 5.1) under my direction; every commit carries the
Co-Authored-Bytrailer.🤖 Generated with Claude Code
Claude Code on behalf of etzel-mes