Skip to content

Use ConcurrentHashMap for gateway asset id map - #3454

Merged
richturner merged 5 commits into
openremote:mainfrom
cgrs5722:fix/gateway-asset-id-map-concurrency
Oct 30, 2026
Merged

richturner merged 5 commits into
openremote:mainfrom
cgrs5722:fix/gateway-asset-id-map-concurrency

Conversation

@cgrs5722

Copy link
Copy Markdown
Contributor

Description

GatewayService.assetIdGatewayIdMap is a plain HashMap that is written under synchronized in processGatewayChildAssetChange, 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 HashMap can throw ConcurrentModificationException while streaming the entry set, or observe inconsistent state.

The fix switches the map to ConcurrentHashMap, consistent with the adjacent gatewayConnectorMap and tunnelInfos fields.

Fixes #3449.

Verification

  • ./gradlew :manager:compileJava passes (JDK 21).
  • A standalone probe replicating the exact access pattern (one thread doing synchronized put/remove as in processGatewayChildAssetChange, another thread doing get + entrySet().stream().filter(...) as in deleteGateway) fails with ConcurrentModificationException on HashMap within ~1000 reads, while ConcurrentHashMap completes hundreds of thousands of operations without error.
  • The integration tests in the test project (e.g. GatewayTest) require a running PostgreSQL + Keycloak stack via docker compose, which I don't have available locally.

🤖 Generated with Claude Code

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>
@CLAassistant

CLAassistant commented Oct 20, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@MartinaeyNL MartinaeyNL added Bug Error or issue in the system java Pull requests that update java code Gateways Topic Gateways: all features related to gateway features labels Oct 20, 2026
@richturner

Copy link
Copy Markdown
Member

Hi @cgrs5722 thanks for picking this up, by switching to ConcurrentHashMap any existing uses of synchronized on the map can also be removed as described in the linked issue.

@richturner

Copy link
Copy Markdown
Member

@cgrs5722 would be great to get this finished off, otherwise we'll have to close this off.

@wborn wborn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@wborn wborn changed the title fix(manager): use ConcurrentHashMap for gateway asset id map Use ConcurrentHashMap for gateway asset id map Oct 29, 2026
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>
@cgrs5722

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review! Both points are addressed in the latest commit:

  • getLocallyRegisteredGatewayId() is now null-safe, preserving the previous HashMap behaviour of falling back to parentId
  • the redundant synchronized blocks in processGatewayChildAssetChange() have been removed

@sonarqubecloud

Copy link
Copy Markdown

@richturner richturner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for making the final updates

@richturner
richturner enabled auto-merge October 30, 2026 09:53

@wborn wborn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the fix!

@richturner
richturner added this pull request to the merge queue Oct 30, 2026
Merged via the queue into openremote:main with commit cad8785 Oct 30, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Error or issue in the system Gateways Topic Gateways: all features related to gateway features java Pull requests that update java code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GatewayService.assetIdGatewayIdMap is a non-thread-safe HashMap read without synchronization

5 participants