HugeGraph version
Current master, verified at commit b9710a7b0319ace41dd3c4ff5fb1165f53ee538e.
Backend and topology:
- HStore backend
- 3 PD nodes
- 3 Store nodes
- 3 Server nodes
- Kubernetes deployment using stable per-Store DNS names
Problem
Replacing a Store pod during a rolling update can leave running HugeGraph Server pods permanently unable to write data to that Store.
The replacement Store pod has the same stable DNS name and Store node ID, but a different pod IP. PD reports the Store as Up with the correct node ID and address. The replacement Store is reachable from the Server pod, but HStore data writes continue to use the failed transport until the Server process is restarted.
This makes routine Store upgrades, node drains, and pod rescheduling capable of taking the graph data plane out of service while the Server pods remain Ready.
Observed behavior
After replacing a Store pod, every affected data write returned HTTP 500 after the HStore retry loop:
HgStoreClientException {sessionInfo: {storeNodeSession: {storeNode:
{address: "hugegraph-store-1.hugegraph-store.<namespace>.svc:8500",
nodeId: 2554185034686971778},
graphName: "DEFAULT/hugegraph/g"}},
reason: "UNAVAILABLE: io exception"}
Relevant stack frames:
NodeTxExecutor.retryingInvoke(NodeTxExecutor.java:404)
NodeTxExecutor.doCommit(NodeTxExecutor.java:120)
NodeTxExecutor.commitTx(NodeTxExecutor.java:100)
NodeTxSessionProxy.commit(NodeTxSessionProxy.java:110)
HstoreSessionsImpl$HstoreSession.commit(HstoreSessionsImpl.java:464)
HstoreStore.commitTx(HstoreStore.java:691)
At the time of failure:
- PD reported all Stores as
Up.
- PD returned the same node ID shown in the exception.
hugegraph-store-1...:8500 accepted TCP connections from inside the Server pod.
- The failure did not recover after more than six minutes.
- Restarting the Server pods restored writes immediately; the same request returned HTTP 201.
- Schema operations continued to work because they did not exercise this HStore commit path.
/versions and /graphs continued responding, so the Server remained Ready and continued receiving traffic.
Expected behavior
After a Store pod is replaced behind the same DNS name, existing Server processes should discard or recover the failed Store transport, resolve the current address, and resume writes without requiring a Server restart.
Source analysis
1. The normal container path enables a SecurityManager
bin/start-hugegraph.sh defaults to:
OPEN_SECURITY_CHECK="true"
The normal Server Docker entrypoint invokes:
./bin/start-hugegraph.sh -j "${JAVA_OPTS:-}" -t 120
It does not pass -s, so the default is retained. bin/hugegraph-server.sh then adds:
-Djava.security.manager=org.apache.hugegraph.security.HugeSecurityManager
The HStore Server image currently uses eclipse-temurin:11-jre-jammy.
There is no networkaddress.cache.ttl or sun.net.inetaddr.ttl setting in the packaged bin/ or conf/ files.
Under Java 11, when a SecurityManager is installed and no positive DNS TTL is explicitly configured, successful InetAddress resolutions are cached forever (-1). This behavior is specified by Java's InetAddress documentation:
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/InetAddress.html
The corresponding OpenJDK implementation is here:
https://github.com/openjdk/jdk11u/blob/master/src/java.base/share/classes/sun/net/InetAddressCachePolicy.java
The JDK uses this infinite default as protection against DNS spoofing or rebinding after an initial trusted resolution. HugeGraph does not explicitly select or document this DNS policy; it is an incidental consequence of enabling a SecurityManager to sandbox Gremlin execution.
2. HStore channels and stubs are retained by target address
AbstractGrpcClient stores channels in a static map keyed by the target string:
protected static Map<String, ManagedChannel[]> channels = new ConcurrentHashMap<>();
Channels are constructed with:
ManagedChannelBuilder.forTarget(target).usePlaintext().build();
No Store-client path removes or shuts down these cached channels when a transport call fails.
The separate channel-pool indexing defect in this class should be reported independently so it does not get buried in this DNS lifecycle issue.
Source:
https://github.com/apache/hugegraph/blob/b9710a7b0319ace41dd3c4ff5fb1165f53ee538e/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/AbstractGrpcClient.java
3. retryingInvoke retries the same cached state
NodeTxExecutor.retryingInvoke() invokes the same Supplier repeatedly after sleeping. It does not invalidate a Store node, session, stub, channel, or DNS state.
The transaction's Store sessions are cached by node ID for the duration of the complete retry loop and are replaced only in doCommit()'s final cleanup.
Source:
https://github.com/apache/hugegraph/blob/b9710a7b0319ace41dd3c4ff5fb1165f53ee538e/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/NodeTxExecutor.java
4. UNAVAILABLE is not handled distinctly
NotifyingExecutor catches all thrown transport failures, reports HgNodeStatus.NOT_WORK, and rethrows. It does not inspect StatusRuntimeException.getStatus().getCode() and has no UNAVAILABLE recovery path.
The resulting notification only calls pdClient.invalidPartitionCache(). It does not invalidate:
HgStoreNodeManager.nodeIdMap
- the cached Store address
- blocking or asynchronous stubs
- cached ManagedChannels
Sources:
5. Re-querying PD does not refresh the existing Store node
Invalidating the partition cache allows the next retry to re-query PD for partition leadership. However, when PD returns the same Store node ID, HgStoreNodeManager.applyNode() immediately returns the existing nodeIdMap entry. It does not call the provider again to retrieve Store metadata or rebuild the Store node.
Source:
https://github.com/apache/hugegraph/blob/b9710a7b0319ace41dd3c4ff5fb1165f53ee538e/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/HgStoreNodeManager.java
6. gRPC attempts DNS refresh, but the JVM cache remains stale
The effective Server dependency resolves gRPC Java to 1.47.0. Its pick_first load balancer requests name resolution when a subchannel reaches TRANSIENT_FAILURE or IDLE:
https://github.com/grpc/grpc-java/blob/v1.47.0/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java
However, DnsNameResolver ultimately resolves through InetAddress.getAllByName():
https://github.com/grpc/grpc-java/blob/v1.47.0/core/src/main/java/io/grpc/internal/DnsNameResolver.java
With the JVM positive DNS cache set effectively to forever by the active SecurityManager, a gRPC refresh receives the previously cached pod IP again. This also explains why restarting only the Server process fixes the failure: it clears the JVM DNS cache and all Store-client caches.
Reproduction outline
- Deploy PD, Store, and Server on Kubernetes with HStore enabled.
- Ensure Server resolves a Store through a stable per-pod/headless-Service DNS name.
- Successfully perform a graph data write.
- Replace that Store pod so the DNS name is unchanged but the pod IP changes.
- Wait for the Store to register as
Up in PD.
- Confirm the Store DNS name and port are reachable from the Server pod.
- Perform another graph data write.
Actual result: writes repeatedly fail with UNAVAILABLE: io exception until Server is restarted.
Expected result: the existing Server reconnects to the replacement Store automatically.
Suggested fixes
Required runtime fix
Configure a finite positive DNS cache TTL for the official Server distribution/container when the SecurityManager is enabled. Prefer a Java security properties override containing, for example:
networkaddress.cache.ttl=30
An immediate OpenJDK 11 workaround is to pass:
-Dsun.net.inetaddr.ttl=30
This was validated on Temurin 11 with the following effective positive TTLs:
| Runtime condition |
Effective positive DNS TTL |
| No SecurityManager, no TTL setting |
30 |
| SecurityManager, no TTL setting |
-1 (forever) |
SecurityManager with -Dsun.net.inetaddr.ttl=30 |
30 |
SecurityManager with -Dnetworkaddress.cache.ttl=30 |
-1 (forever) |
networkaddress.cache.ttl is a Java security property. Passing it only as an ordinary -D system property does not change Java 11's InetAddress cache policy. The supported distribution-level fix should set it through Java security properties; the chart-level JVM workaround should use -Dsun.net.inetaddr.ttl=30.
Java 11 checks the networkaddress.cache.ttl security property before the sun.net.inetaddr.ttl system-property fallback. Therefore, if the distribution later configures networkaddress.cache.ttl through Java security properties, that distribution value takes precedence and the chart's -Dsun.net.inetaddr.ttl=30 becomes a harmless no-op. It will not override or conflict with the upstream policy.
Compatibility and security tradeoff
For a healthy cluster whose Store addresses do not change, making the TTL finite does not tear down established channels or force working connections to re-resolve. DNS cache expiration is lazy: it permits a later resolution to consult DNS again, but does not itself schedule a DNS query every 30 seconds. In this client, gRPC requests name resolution when the channel encounters failure or enters idle state.
The intended observable change is that a new Store IP behind an unchanged DNS name can eventually be discovered instead of the old IP remaining cached for the lifetime of the Server JVM.
This is not entirely security-neutral. The SecurityManager-dependent infinite TTL is a DNS spoofing and rebinding defense, so changing it to a finite value reopens a bounded re-resolution window. The proposed tradeoff is appropriate here because:
HugeSecurityManager exists to sandbox Gremlin script execution; the repository contains no indication that its incidental infinite DNS cache is a deliberate Store-transport security control.
- HStore names are obtained from PD metadata rather than arbitrary user-controlled request hostnames.
30 seconds is OpenJDK 11's own default positive TTL when no SecurityManager is installed.
- The negative DNS cache policy is separate (
networkaddress.cache.negative.ttl, normally 10 seconds) and is not changed by this proposal.
If the project considers PD-provided Store addresses untrusted, that trust boundary should be addressed directly through PD authentication, transport security, and address validation rather than by permanently pinning the first DNS answer received by each Server process.
Optional Store-client hardening
- Detect
Status.Code.UNAVAILABLE explicitly.
- Atomically evict the target's cached blocking stubs, asynchronous stubs, and ManagedChannels.
- Shut down evicted ManagedChannels.
- Invalidate or refresh the corresponding cached
HgStoreNode so address changes for the same node ID can be observed.
- Reset connection backoff or rebuild the target before the next application retry.
Channel invalidation alone will not solve the stable-DNS case while the JVM DNS cache remains infinite, so the finite TTL is required.
Kubernetes readiness impact
The official HStore image health check currently calls /versions. That endpoint remains healthy during this failure because the Server process and control-plane APIs are available even though HStore data writes are unavailable.
It would be useful to expose a non-mutating data-plane readiness endpoint that verifies the Server can reach the current HStore partition leaders. Helm charts could then use that endpoint instead of a process-only readiness check.
Operational severity
This failure is triggered by ordinary Kubernetes operations such as Store upgrades, pod rescheduling, and node drains. It does not self-heal, is invisible to the current readiness probe, and requires restarting otherwise healthy Server pods to restore writes.
HugeGraph version
Current
master, verified at commitb9710a7b0319ace41dd3c4ff5fb1165f53ee538e.Backend and topology:
Problem
Replacing a Store pod during a rolling update can leave running HugeGraph Server pods permanently unable to write data to that Store.
The replacement Store pod has the same stable DNS name and Store node ID, but a different pod IP. PD reports the Store as
Upwith the correct node ID and address. The replacement Store is reachable from the Server pod, but HStore data writes continue to use the failed transport until the Server process is restarted.This makes routine Store upgrades, node drains, and pod rescheduling capable of taking the graph data plane out of service while the Server pods remain Ready.
Observed behavior
After replacing a Store pod, every affected data write returned HTTP 500 after the HStore retry loop:
Relevant stack frames:
At the time of failure:
Up.hugegraph-store-1...:8500accepted TCP connections from inside the Server pod./versionsand/graphscontinued responding, so the Server remained Ready and continued receiving traffic.Expected behavior
After a Store pod is replaced behind the same DNS name, existing Server processes should discard or recover the failed Store transport, resolve the current address, and resume writes without requiring a Server restart.
Source analysis
1. The normal container path enables a SecurityManager
bin/start-hugegraph.shdefaults to:OPEN_SECURITY_CHECK="true"The normal Server Docker entrypoint invokes:
./bin/start-hugegraph.sh -j "${JAVA_OPTS:-}" -t 120It does not pass
-s, so the default is retained.bin/hugegraph-server.shthen adds:The HStore Server image currently uses
eclipse-temurin:11-jre-jammy.There is no
networkaddress.cache.ttlorsun.net.inetaddr.ttlsetting in the packagedbin/orconf/files.Under Java 11, when a SecurityManager is installed and no positive DNS TTL is explicitly configured, successful
InetAddressresolutions are cached forever (-1). This behavior is specified by Java'sInetAddressdocumentation:https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/InetAddress.html
The corresponding OpenJDK implementation is here:
https://github.com/openjdk/jdk11u/blob/master/src/java.base/share/classes/sun/net/InetAddressCachePolicy.java
The JDK uses this infinite default as protection against DNS spoofing or rebinding after an initial trusted resolution. HugeGraph does not explicitly select or document this DNS policy; it is an incidental consequence of enabling a SecurityManager to sandbox Gremlin execution.
2. HStore channels and stubs are retained by target address
AbstractGrpcClientstores channels in a static map keyed by the target string:Channels are constructed with:
No Store-client path removes or shuts down these cached channels when a transport call fails.
The separate channel-pool indexing defect in this class should be reported independently so it does not get buried in this DNS lifecycle issue.
Source:
https://github.com/apache/hugegraph/blob/b9710a7b0319ace41dd3c4ff5fb1165f53ee538e/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/AbstractGrpcClient.java
3.
retryingInvokeretries the same cached stateNodeTxExecutor.retryingInvoke()invokes the sameSupplierrepeatedly after sleeping. It does not invalidate a Store node, session, stub, channel, or DNS state.The transaction's Store sessions are cached by node ID for the duration of the complete retry loop and are replaced only in
doCommit()'s final cleanup.Source:
https://github.com/apache/hugegraph/blob/b9710a7b0319ace41dd3c4ff5fb1165f53ee538e/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/NodeTxExecutor.java
4.
UNAVAILABLEis not handled distinctlyNotifyingExecutorcatches all thrown transport failures, reportsHgNodeStatus.NOT_WORK, and rethrows. It does not inspectStatusRuntimeException.getStatus().getCode()and has noUNAVAILABLErecovery path.The resulting notification only calls
pdClient.invalidPartitionCache(). It does not invalidate:HgStoreNodeManager.nodeIdMapSources:
5. Re-querying PD does not refresh the existing Store node
Invalidating the partition cache allows the next retry to re-query PD for partition leadership. However, when PD returns the same Store node ID,
HgStoreNodeManager.applyNode()immediately returns the existingnodeIdMapentry. It does not call the provider again to retrieve Store metadata or rebuild the Store node.Source:
https://github.com/apache/hugegraph/blob/b9710a7b0319ace41dd3c4ff5fb1165f53ee538e/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/HgStoreNodeManager.java
6. gRPC attempts DNS refresh, but the JVM cache remains stale
The effective Server dependency resolves gRPC Java to 1.47.0. Its
pick_firstload balancer requests name resolution when a subchannel reachesTRANSIENT_FAILUREorIDLE:https://github.com/grpc/grpc-java/blob/v1.47.0/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java
However,
DnsNameResolverultimately resolves throughInetAddress.getAllByName():https://github.com/grpc/grpc-java/blob/v1.47.0/core/src/main/java/io/grpc/internal/DnsNameResolver.java
With the JVM positive DNS cache set effectively to forever by the active SecurityManager, a gRPC refresh receives the previously cached pod IP again. This also explains why restarting only the Server process fixes the failure: it clears the JVM DNS cache and all Store-client caches.
Reproduction outline
Upin PD.Actual result: writes repeatedly fail with
UNAVAILABLE: io exceptionuntil Server is restarted.Expected result: the existing Server reconnects to the replacement Store automatically.
Suggested fixes
Required runtime fix
Configure a finite positive DNS cache TTL for the official Server distribution/container when the SecurityManager is enabled. Prefer a Java security properties override containing, for example:
networkaddress.cache.ttl=30An immediate OpenJDK 11 workaround is to pass:
This was validated on Temurin 11 with the following effective positive TTLs:
30-1(forever)-Dsun.net.inetaddr.ttl=3030-Dnetworkaddress.cache.ttl=30-1(forever)networkaddress.cache.ttlis a Java security property. Passing it only as an ordinary-Dsystem property does not change Java 11'sInetAddresscache policy. The supported distribution-level fix should set it through Java security properties; the chart-level JVM workaround should use-Dsun.net.inetaddr.ttl=30.Java 11 checks the
networkaddress.cache.ttlsecurity property before thesun.net.inetaddr.ttlsystem-property fallback. Therefore, if the distribution later configuresnetworkaddress.cache.ttlthrough Java security properties, that distribution value takes precedence and the chart's-Dsun.net.inetaddr.ttl=30becomes a harmless no-op. It will not override or conflict with the upstream policy.Compatibility and security tradeoff
For a healthy cluster whose Store addresses do not change, making the TTL finite does not tear down established channels or force working connections to re-resolve. DNS cache expiration is lazy: it permits a later resolution to consult DNS again, but does not itself schedule a DNS query every 30 seconds. In this client, gRPC requests name resolution when the channel encounters failure or enters idle state.
The intended observable change is that a new Store IP behind an unchanged DNS name can eventually be discovered instead of the old IP remaining cached for the lifetime of the Server JVM.
This is not entirely security-neutral. The SecurityManager-dependent infinite TTL is a DNS spoofing and rebinding defense, so changing it to a finite value reopens a bounded re-resolution window. The proposed tradeoff is appropriate here because:
HugeSecurityManagerexists to sandbox Gremlin script execution; the repository contains no indication that its incidental infinite DNS cache is a deliberate Store-transport security control.30seconds is OpenJDK 11's own default positive TTL when no SecurityManager is installed.networkaddress.cache.negative.ttl, normally 10 seconds) and is not changed by this proposal.If the project considers PD-provided Store addresses untrusted, that trust boundary should be addressed directly through PD authentication, transport security, and address validation rather than by permanently pinning the first DNS answer received by each Server process.
Optional Store-client hardening
Status.Code.UNAVAILABLEexplicitly.HgStoreNodeso address changes for the same node ID can be observed.Channel invalidation alone will not solve the stable-DNS case while the JVM DNS cache remains infinite, so the finite TTL is required.
Kubernetes readiness impact
The official HStore image health check currently calls
/versions. That endpoint remains healthy during this failure because the Server process and control-plane APIs are available even though HStore data writes are unavailable.It would be useful to expose a non-mutating data-plane readiness endpoint that verifies the Server can reach the current HStore partition leaders. Helm charts could then use that endpoint instead of a process-only readiness check.
Operational severity
This failure is triggered by ordinary Kubernetes operations such as Store upgrades, pod rescheduling, and node drains. It does not self-heal, is invisible to the current readiness probe, and requires restarting otherwise healthy Server pods to restore writes.