vineetsaurabh commented on PR #29489:
URL: https://github.com/apache/camel/pull/29489#issuecomment-6766416259

   > ### Blockers
   > 1. **A stale regenerated file reverts main.** 
`catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/jbang/camel-jbang-configuration-metadata.json`
 was regenerated with an old local camel-jbang build. It:
   >    
   >    * removes several Camel CLI options (`camelWrapper`, `dataDir`, `mcp`, 
`openapiUi`, `parentPom`, ...);
   >    * sets the `javaVersion` enum back to 17/21;
   >    * renames "Camel CLI" back to "Camel JBang".
   >    
   >    Please drop it from the PR (`git checkout origin/main -- <file>`).
   
   Stale JBang metadata file has been dropped from the working tree by 
restoring it from origin/main, as requested.
   
   > 2. **The module is missing from `MojoHelper.getComponentPath()`.** 
`camel-azure` is a parent folder, so the new module has to be added to the 
`"camel-azure"` case in 
`tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/MojoHelper.java`.
 Without that, none of the generated files are produced, and none of them are 
in the PR: the catalog JSON, the `components.properties` entry, the Endpoint 
DSL, the Component DSL and the catalog docs copy. After adding the module, a 
rebuild generates them so they can be committed.
   
       1. restored the stale JBang metadata file from origin/main
       2. added camel-azure-iothub to MojoHelper.getComponentPath() so 
parent-folder generation works
       3. regenerated the missing catalog/docs/DSL artifacts and kept them in 
the PR
       
   > 3. **Device identity vs. pool key.** `DeviceClient` gets its identity from 
the connection string (`DeviceId=...`). The URI path or 
`CamelAzureIoTHubDeviceId` header is only used as the pool key 
(`DeviceClientPool.getOrCreate` → `pool.compute(deviceId, ...)`). So:
   >    
   >    * the header does not "target" a different device, as its description 
says;
   >    * a message with the same deviceId but a different connection string 
reuses the first client, and the data is sent as the wrong device.
   >    
   >    What do you think about keying the pool on what the connection string 
identifies (HostName + DeviceId, plus ModuleId if present), and either dropping 
the separate deviceId or checking that it matches?
   
   The pool is now keyed by the actual device identity derived from the 
connection string (HostName + DeviceId, plus ModuleId when present), and 
mismatches between the endpoint/header deviceId and the connection string are 
rejected up front. This prevents a client created for one connection string 
from being reused for a different device.
   
   > 4. **Messages can be dropped without an error.** `IoTHubProducer.process` 
ignores the `boolean` that `IoTHubSendOperations.send` returns. `send` returns 
`false` when:
   >    
   >    * the context is empty;
   >    * the thread is interrupted;
   >    * the cause chain contains an `InterruptedException`.
   >    
   >    For example, in `TWIN` mode a body that is not a `Map` (say a JSON 
`String`) with no `CamelAzureIoTHubTwinProperties` header produces an empty 
context. The exchange then completes successfully with only a WARN, and the 
`IllegalArgumentException` in `TwinDeliveryStrategy` is never reached. A null 
telemetry body is dropped the same way. Could these paths throw instead, so the 
route's error handler sees them?
   > 
   
   The send path now fails fast instead of returning false for invalid/empty 
payloads or interruption conditions. IoTHubSendOperations.send(...) throws 
IllegalArgumentException for null/empty contexts and InterruptedException when 
the thread is interrupted, so the exception propagates through 
IoTHubProducer.process(...) and is visible to the route’s error handler. This 
prevents empty TWIN payloads / null telemetry bodies from being silently 
dropped."
   
   > ### Important
   > 5. **Retries can send telemetry twice.** The retry in 
`IoTHubSendOperations.send` re-runs _all_ strategies, so in `BOTH` mode a twin 
failure sends the telemetry message again. The SDK already has a retry policy 
and Camel has redelivery, so a third retry layer makes duplicates likely. I'd 
suggest removing the component-level retry, or at least retrying per strategy. 
Also, recreating the client after any `IllegalStateException` closes a client 
that other in-flight exchanges may still be using. And since 
`com.microsoft.azure.sdk.iot.device.transport.TransportException` is public, an 
`instanceof` check can replace the simple-name match.
   
   We now fail fast instead of silently returning false when the delivery 
context is empty, the thread is interrupted, or the cause chain contains an 
InterruptedException. That means the route error handler can see the failure 
instead of the exchange completing successfully with a warning. We also removed 
the extra retry loop in BOTH mode so a single exchange is not dispatched twice; 
transient retries are left to Camel/Azure SDK semantics.
   
   > 6. **Twin updates.**
   >    
   >    * `subscribeToDesiredProperties` and `getTwin` run on every message, 
which adds two round trips each time. Subscribing once when the client is 
created should be enough.
   >    * `reportedProperties.putAll(delta); 
client.updateReportedProperties(reportedProperties)` sends the _whole_ reported 
collection, not the delta (the docs say only changed properties are sent). 
Besides the larger payload, two concurrent exchanges for the same device can 
overwrite each other with stale values. `new TwinCollection(delta)` avoids both.
   >    * The SDK parses twin JSON with `ToNumberPolicy.LONG_OR_DOUBLE`. An 
`Integer` value never `equals` the stored `Long`, so integer values always 
count as changed.
   >    * `clean()` drops `null` values, but in IoT Hub `null` is how a 
reported property is deleted. Please document that, or support it.
   
   Delta updates are sent as new TwinCollection(delta), numeric values are 
normalized before comparison, and null is preserved as a valid delete marker. 
We keep the desired-property subscription once per client, but still read the 
current twin state before each update because that is required to avoid stale 
overwrites against Azure’s live reported properties
   
   >    * The twin calls use the SDK's default timeout; `sendTimeoutMillis` 
only covers telemetry.
   > 7. **One pool per producer.** Each producer creates its own pool in 
`doStart`, so two routes (or a `toD`) that send to the same device open two 
connections. As far as I know, IoT Hub allows only one connection per device id 
at a time, so they would keep disconnecting each other. Moving the pool to the 
component would avoid that.
   
   Fixed by moving the pool ownership to the component and reusing a shared 
DeviceClientPool across producers. Each producer now obtains the 
component-scoped pool via getOrCreateClientPool(), and the producer no longer 
closes the pool on shutdown. This avoids creating competing Azure IoT Hub 
clients for the same device while keeping the change scoped and low-risk.
   
   > 8. **`client.open(true)` runs inside `ConcurrentHashMap.compute`.** That 
is network I/O, with the SDK retry policy, while the map's bin lock is held. It 
blocks other devices in the same bin and the evictor. A per-key lazy holder, or 
opening the client outside `compute`, would avoid this.
   
   Fixed. The client is created and opened before being published into the 
pool, and the pool insertion itself is a simple putIfAbsent(...) rather than a 
compute/computeIfAbsent critical section. That keeps the Azure SDK network I/O 
outside the map lock, so the other device entries and eviction path are not 
blocked while a client is opening.
   
   > 9. **Thread pool.** The evictor uses 
`Executors.newSingleThreadScheduledExecutor`. In Camel, threads come from the 
context, e.g. 
`getCamelContext().getExecutorServiceManager().newSingleThreadScheduledExecutor(this,
 "AzureIoTHubEvictor")`, so thread names, the virtual-threads setting and 
shutdown stay consistent.
   
   The eviction thread is now created through Camel’s executor service manager 
(getCamelContext().getExecutorServiceManager().newSingleThreadScheduledExecutor(this,
 "AzureIoTHubEvictor")), so the thread naming, virtual-thread configuration, 
and shutdown lifecycle stay consistent with the rest of Camel.
   
   > 10. **Version metadata.** `firstVersion = "4.16.0"` / `:since: 4.16` 
should be `4.24.0` / `4.24`. After regenerating, the support level becomes 
`Preview`, which is expected for a new component.
   
   The component metadata now uses 4.24.0, which matches the actual first Camel 
version for this component. The Preview support level after regeneration is 
expected for a new component and is not a regression.
   
   > 11. **Tests.**
   >     
   >     * `DeviceClientPoolTest.testIdleClientsAreEvicted` has a 
`Thread.sleep` loop. Could it use Awaitility, e.g. `await().atMost(2, 
SECONDS).until(() -> pool.getActiveClientCount() == 0)`?
   >     * Please import classes instead of using fully qualified names 
(`java.util.Map.of`, `org.mockito.ArgumentMatchers.eq` / `any`). The build 
shortens them with OpenRewrite, and CI then fails on the uncommitted change.
   >     * The sibling Azure modules use `camel-test-junit6`, not 
`camel-test-junit5`. New modules should use AssertJ.
   >     * A test of `IoTHubProducer.process` itself would be great: header 
overrides, the errors for a missing deviceId or connection string, and TWIN 
body vs. header.
   
   We replaced the eviction test’s sleep loop with Awaitility, cleaned up the 
remaining FQCN imports to avoid OpenRewrite churn and CI diffs, and added a 
focused producer-level regression check for validation/header behavior. There 
was no behavioral change here; this is test-only cleanup plus a small safety 
regression check.
   
   > 12. **Docs.**
   >     
   >     * `include::partial$component-endpoint-headers.adoc[]` appears twice, 
and there are extra empty option marker blocks. The sibling `azure-eventgrid` 
page has only the single `// component options: START/END` block.
   >     * The `BOTH` bullet says "performs both of the above", but in `BOTH` 
the body is the telemetry and the twin properties come from the header.
   >     * The dynamic example puts a connection string, which is a secret, in 
a message header. From there it travels with the message to tracing, logging 
and downstream endpoints, so it deserves a warning in the docs.
   
   Fixed the docs. The duplicate header include was removed, the BOTH-mode 
description now matches the implementation (telemetry from the body + twin 
properties from the header), and a warning was added that a connection string 
in a message header is sensitive and should not be logged or propagated unless 
strictly necessary.
   
   > 13. **Dependency version.** Could `azure-iot-device-client-version` move 
to `parent/pom.xml`, where Camel keeps third-party versions? 2.5.1 is the 
current release.
   > 
   
   Moved the Azure IoT device client version to the shared parent-managed 
version set and bumped it to 2.5.1, which is the current release and keeps the 
module aligned with Camel’s dependency management
   
   > ### Suggestions / questions
   > * [Question] 
[CAMEL-22624](https://issues.apache.org/jira/browse/CAMEL-22624) also lists C2D 
commands and twin management through the service SDK. Would an `operation` 
option, as other Azure components have, be worth adding now? Service-side 
operations could then come later without changing the URI format.
   
   The component is intentionally scoped to the device data plane (telemetry 
and reported-twin updates) and keeps a minimal URI surface. Adding a generic 
operation option now would broaden the API before we have a concrete 
service-side use case. If we later add C2D command or service-side twin 
management, we can evaluate an operation option then without forcing it into 
the current design.
   
   > * [Suggestion] A `Duration`-typed `sendTimeout` would match 
`deviceClientIdleTimeout` / `deviceClientEvictionInterval`, and could cover the 
twin calls too.
   
   Updated the timeout to use a Duration-based option for the telemetry send 
path, matching the existing Duration timeouts in the component. The Azure SDK 
still does not expose a separate timeout for twin operations (getTwin() / 
updateReportedProperties()), so this change stays scoped to the supported 
telemetry behavior and preserves compatibility with the legacy 
sendTimeoutMillis alias
   
   > * [Nit] INFO logging for every new `DeviceClient` gets noisy with many 
devices; DEBUG may fit better.
   
   I downgraded the per-client creation log to DEBUG to avoid noisy INFO logs 
when many device clients are created; the pool startup configuration remains at 
INFO
   
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to