Repository navigation
Use ConcurrentHashMap for gateway asset id map - #3454
Conversation
assetIdGatewayIdMap is written under synchronization in processGatewayChildAssetChange but read unsynchronized in onAttributeEventIntercepted, deleteGateway and getLocallyRegisteredGatewayId, which constitutes a data race (e.g. ConcurrentModificationException while streaming the entry set). Use a ConcurrentHashMap like the adjacent gatewayConnectorMap and tunnelInfos fields. Fixes openremote#3449 Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Hi @cgrs5722 thanks for picking this up, by switching to |
|
@cgrs5722 would be great to get this finished off, otherwise we'll have to close this off. |
wborn
left a comment
There was a problem hiding this comment.
This is an additional AI-assisted review.
Switching to ConcurrentHashMap addresses the concurrency issue described in #3449, but introduces a regression when looking up a null asset ID, causing the integration tests to fail during application setup. See the inline comment for details.
Also, as already noted by @richturner, the synchronized blocks in processGatewayChildAssetChange() (lines 928–935) can now be removed. Both operations are already thread-safe with ConcurrentHashMap, so the switch can be simplified to:
switch (persistenceEvent.getCause()) {
case CREATE, UPDATE -> assetIdGatewayIdMap.put(childAsset.getId(), gatewayId);
case DELETE -> assetIdGatewayIdMap.remove(childAsset.getId());
}Please address the null handling, remove the redundant synchronization, and rerun the integration tests.
Handle null asset ID in getLocallyRegisteredGatewayId() since ConcurrentHashMap.get(null) throws NPE where HashMap.get(null) returned null, breaking integration tests during app setup. Drop the synchronized blocks in processGatewayChildAssetChange() as ConcurrentHashMap operations are individually thread safe. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Thanks for the detailed review! Both points are addressed in the latest commit:
|
|
richturner
left a comment
There was a problem hiding this comment.
Thanks for making the final updates



Description
GatewayService.assetIdGatewayIdMapis a plainHashMapthat is written undersynchronizedinprocessGatewayChildAssetChange, but read without any synchronization from:onAttributeEventIntercepted(get, attribute event interceptor thread)deleteGateway(entrySet().stream()while writes can happen concurrently)getLocallyRegisteredGatewayId(get, called from the Camel route predicate)This is a data race: unsynchronized reads against synchronized writes on a
HashMapcan throwConcurrentModificationExceptionwhile streaming the entry set, or observe inconsistent state.The fix switches the map to
ConcurrentHashMap, consistent with the adjacentgatewayConnectorMapandtunnelInfosfields.Fixes #3449.
Verification
./gradlew :manager:compileJavapasses (JDK 21).synchronizedput/remove as inprocessGatewayChildAssetChange, another thread doingget+entrySet().stream().filter(...)as indeleteGateway) fails withConcurrentModificationExceptiononHashMapwithin ~1000 reads, whileConcurrentHashMapcompletes hundreds of thousands of operations without error.testproject (e.g.GatewayTest) require a running PostgreSQL + Keycloak stack via docker compose, which I don't have available locally.🤖 Generated with Claude Code