bitflicker64 opened a new issue, #3124:
URL: https://github.com/apache/hugegraph/issues/3124

   ## 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:
   
   ```text
   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:
   
   ```text
   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:
   
   ```bash
   OPEN_SECURITY_CHECK="true"
   ```
   
   The normal Server Docker entrypoint invokes:
   
   ```bash
   ./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:
   
   ```bash
   -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:
   
   ```java
   protected static Map<String, ManagedChannel[]> channels = new 
ConcurrentHashMap<>();
   ```
   
   Channels are constructed with:
   
   ```java
   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:
   
   - 
https://github.com/apache/hugegraph/blob/b9710a7b0319ace41dd3c4ff5fb1165f53ee538e/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/NotifyingExecutor.java
   - 
https://github.com/apache/hugegraph/blob/b9710a7b0319ace41dd3c4ff5fb1165f53ee538e/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/HgStoreNodePartitionerImpl.java
   
   ### 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
   
   1. Deploy PD, Store, and Server on Kubernetes with HStore enabled.
   2. Ensure Server resolves a Store through a stable per-pod/headless-Service 
DNS name.
   3. Successfully perform a graph data write.
   4. Replace that Store pod so the DNS name is unchanged but the pod IP 
changes.
   5. Wait for the Store to register as `Up` in PD.
   6. Confirm the Store DNS name and port are reachable from the Server pod.
   7. 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:
   
   ```properties
   networkaddress.cache.ttl=30
   ```
   
   An immediate OpenJDK 11 workaround is to pass:
   
   ```text
   -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.
   


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