Repository navigation
HDDS-16300. Allow OM to dynamically reconfigure its SCM node list without a restart - #11218
Conversation
16ca562 to
3697e43
Compare
chihsuan
left a comment
There was a problem hiding this comment.
Thanks for working on this! @hani-fouladgar I left a few inline comments, please see them for details.
Also, should the PR description say secure clusters still need an OM restart for a full SCM migration?
| SCMProxyInfo newInfo = scmProxyInfoMap.get(nodeId); | ||
| if (newInfo == null | ||
| || !newInfo.getAddress().equals(entry.getValue().getAddress())) { | ||
| ProxyInfo<T> staleProxy = scmProxies.remove(nodeId); |
There was a problem hiding this comment.
Does stopping the proxy guarantee that the next call uses the updated endpoint? The retry handler keeps the old proxy until a failover, and that proxy can still make calls. So a removed but online SCM keeps serving OM calls. Could we add a regression test for this?
There was a problem hiding this comment.
Good catch — it does not, and I've documented the limitation on changeConfig: the wrapping RetryInvocationHandler keeps its last-fetched proxy and re-fetches only on failover, so a removed-but-online SCM can keep serving in-flight calls until the next failover. The new endpoint is guaranteed only from the next fetch. Added testAddressChangeEvictsCachedProxy to cover the provider-level eviction.
| * dials the fresh address. If the new configuration is incomplete this throws | ||
| * and leaves the current state intact. | ||
| */ | ||
| public synchronized void changeConfig() { |
There was a problem hiding this comment.
Could we resolve addresses and stop proxies outside the lock, like refreshProxyAddressIfChanged does? If DNS is slow for a new SCM hostname, other SCM calls may be blocked.
There was a problem hiding this comment.
Done. Split out a pure buildConfigs() that does the DNS resolution and touches no shared state; changeConfig() now calls it before taking the monitor, and defers RPC.stopProxy until after the monitor is released. A slow resolver for a new SCM host no longer blocks concurrent SCM calls. Mirrors refreshProxyAddressIfChanged.
| SCMBlockLocationFailoverProxyProvider provider = | ||
| new SCMBlockLocationFailoverProxyProvider(conf); | ||
| // Point the current proxy at the node that is about to be removed. | ||
| provider.changeCurrentProxy("scm3"); |
There was a problem hiding this comment.
I noticed changeCurrentProxy moves to the next node, so the current proxy here is actually scm1, not scm3. Should we assert it is scm3 before the reload, so the removal path is really tested?
There was a problem hiding this comment.
Fixed. changeCurrentProxy advances to the next ring node, so I now target the node before scm3 and assertEquals("scm3", provider.getCurrentProxySCMNodeId()) before reload, so the removal path is actually exercised.
| * the involved nodes. | ||
| * | ||
| * Scope: only the block and container proxies are reloaded here. Changing an | ||
| * address key alone does not trigger a reload; touch the node list to apply |
There was a problem hiding this comment.
Just curious, is changing only an address key expected to not take effect? The configuration is updated and reconfig reports success, but OM still connects to the old address. Also, adding a node may need a second reconfig start if the node list is applied before the address.
There was a problem hiding this comment.
Real bug — fixed. Prefix-registered address keys have no per-key reload function, so an address-only change was never applied. reconfScmNodes now defers on ConfigurationException (keeps the value instead of rolling back), and a new reconfiguration-complete callback reloadScmProxiesOnReconfig reloads once the whole batch is applied. So an address-only change takes effect, and adding a node works in a single reconfig start regardless of key order. Covered by the new testReconfigureScmAddressReloadsProxies integration test.
There was a problem hiding this comment.
Thanks for the fix! I tried the defer path. With the address key missing, status still says SUCCESS but the providers keep the old list. The live conf also keeps a node without an address, so getServiceList() starts throwing. Could we throw here instead, like the datanode side, so it shows FAILED and can be retried?
There was a problem hiding this comment.
You were right: the defer left a node without an address in the live conf while reporting SUCCESS, breaking getServiceList(). reconfScmNodes now captures the previous node-list value, and on a reload ConfigurationException it restores the previous value and rethrows. ReconfigurationHandler wraps that into a ReconfigurationException, so the reconfig is reported FAILED and is retriable, and the live conf never keeps an SCM without a resolvable address. This mirrors the datanode's "don't record what didn't take effect" semantics (the datanode returns only its effective node set; the OM can't skip per-node because changeConfig() rebuilds the whole proxy list atomically, so roll-back-and-fail is the equivalent).
| } | ||
|
|
||
| /** | ||
| * Reload the SCM node list and their addresses from the (already updated) |
There was a problem hiding this comment.
nit: Could we trim the comments a bit? The same reasoning is repeated in a few places, here and in OzoneManager for example. Keeping each point once would be enough.
There was a problem hiding this comment.
Done. Removed the duplicated "applied in either order" reasoning here; it now lives in one place — the reconfScmNodes javadoc in OzoneManager.
Added to the PR description. |
chihsuan
left a comment
There was a problem hiding this comment.
Thanks for the updates! @hani-fouladgar The provider side looks good now. 👍
I left a follow-up on the defer thread and two inline notes.
| boolean scmProxyKeyChanged = changedProperties.keySet().stream() | ||
| .anyMatch(key -> key.equals(scmNodesKey) || key.startsWith(scmAddressPrefix)); | ||
| if (scmProxyKeyChanged) { | ||
| scmClient.reloadScmNodes(); |
There was a problem hiding this comment.
Should we catch and log here? If the reload throws, the remaining complete callbacks (tracing, logging) are skipped.
|
|
||
| Map<String, Boolean> changed = new HashMap<>(); | ||
| changed.put(scmAddrKey, true); | ||
| om.reloadScmProxiesOnReconfig(changed, conf); |
There was a problem hiding this comment.
Could we also cover the nodes-before-address case? reconfigureProperty never fires the complete callback, so its wiring is not exercised by these tests.
There was a problem hiding this comment.
Thanks! I think this still sets both keys before calling the callback, so the nodes-before-address order in a real batch is not exercised yet.
There was a problem hiding this comment.
You were right: my old test set both keys then called the callback, which reproduces the address-first end state, not the nodes-before-address ordering. Replaced it with testReconfigureAddScmNodeNodesBeforeAddress, which applies the keys in the real batch order:
- Node list applied first (address not set yet) → the property fails with ReconfigurationException and rolls back → asserts the node is not added.
- The new SCM's address applied (prefix key, no reload of its own).
- Node list reapplied → now resolves → asserts the node is added on both providers.
chihsuan
left a comment
There was a problem hiding this comment.
Thanks @hani-fouladgar I think two cases remain, both from validating one key at a time.
Adding an SCM can fail if the node list is applied before its address, since reconfScmNodes only sees the live conf at that point. An invalid address change can also report SUCCESS.
I wonder if these keys should be validated as one group before anything is written. That would need a hook in ReconfigurationHandler / ReconfigurableBase. Since it touches the shared mechanism, I think a committer's take would help on whether to do it here or in a follow-up.
| } | ||
|
|
||
| private static void assertResolvedHost( | ||
| org.apache.hadoop.hdds.scm.proxy.SCMFailoverProxyProviderBase<?> provider, |
There was a problem hiding this comment.
nit: Could we import SCMFailoverProxyProviderBase here instead of the fully qualified name?
There was a problem hiding this comment.
Added import org.apache.hadoop.hdds.scm.proxy.SCMFailoverProxyProviderBase; and changed the assertResolvedHost signature to use the short name.
|
|
||
| Map<String, Boolean> changed = new HashMap<>(); | ||
| changed.put(scmAddrKey, true); | ||
| om.reloadScmProxiesOnReconfig(changed, conf); |
There was a problem hiding this comment.
Thanks! I think this still sets both keys before calling the callback, so the nodes-before-address order in a real batch is not exercised yet.
Thanks, that's a fair read — both cases are inherent to per-key validation, and I agree grouped/atomic validation of ozone.scm.nodes. together with the ozone.scm.address..* keys is the clean fix. What this PR guarantees today is the safety property: an unresolvable node list fails and is rolled back (visible as FAILED, retriable), so the live configuration never ends up holding an SCM without an address, and getServiceList() stays consistent. The two residuals are usability, not correctness:
Closing both properly means adding a batch/pre-validation hook to ReconfigurableBase/ReconfigurationHandler, which is shared by the datanode, OM, and SCM. Since that changes the shared mechanism, I agree it's better as a separate change with a committer's input rather than expanding this PR's scope. I'll file a follow-up HDDS Jira for grouped SCM-config validation and link it here — happy to take it on once there's a committer's steer on the approach. Does deferring those two cases to that follow-up sound reasonable for this PR? |
chihsuan
left a comment
There was a problem hiding this comment.
I'll file a follow-up HDDS Jira for grouped SCM-config validation and link it here, happy to take it on once there's a committer's steer on the approach. Does deferring those two cases to that follow-up sound reasonable for this PR?
Thanks @hani-fouladgar Sounds reasonable to me. I left three inline notes, otherwise looks good to me, pending a committer's review.
| try { | ||
| scmClient.reloadScmNodes(); | ||
| LOG.info("Reloaded SCM proxy configuration for {} : {}", scmNodesKey, value); | ||
| } catch (ConfigurationException e) { |
There was a problem hiding this comment.
Could we widen this catch to RuntimeException? I noticed NetUtils.createSocketAddr throws IllegalArgumentException for an address like host:9860. That skips the rollback, so the live conf keeps the bad node list.
| scmClient.reloadScmNodes(); | ||
| LOG.info("Reloaded SCM failover proxies after reconfiguration of {} / {}*", | ||
| scmNodesKey, scmAddressPrefix); | ||
| } catch (ConfigurationException e) { |
There was a problem hiding this comment.
Same here, should this catch RuntimeException too? A malformed address-only change would still escape and skip the tracing and logging callbacks.
|
|
||
| Map<String, Boolean> changed = new HashMap<>(); | ||
| changed.put(scmAddrKey, true); | ||
| om.reloadScmProxiesOnReconfig(changed, conf); |
There was a problem hiding this comment.
I wonder if we could drive this through the real complete-callback path instead? Calling the method directly means the registerCompleteCallback line is not covered, and it is also what forces reloadScmProxiesOnReconfig to be public.
There was a problem hiding this comment.
Good question — I looked into driving this through the real complete-callback path, but it isn't reachable from a mini-cluster. The reconfiguration-complete callbacks only fire from the async ReconfigurationThread (startReconfigurationTask()), which reads a fresh new OzoneConfiguration() off ozone-site.xml from disk. The mini-cluster never rewrites that file, so an address-only change made in-memory on the live conf is invisible to that path — the callback would diff two identical on-disk configs and do nothing.
The synchronous reconfigureProperty path (which the other tests use) doesn't fire complete callbacks at all — it only runs the per-property function, and the address keys are prefix-registered with identity(), so there's no per-key hook to exercise. That's why an address-only change is applied by the complete callback rather than the per-property path.
So calling reloadScmProxiesOnReconfig directly is the only way to cover the address-only reload in this harness, and it's what keeps the method @VisibleForTesting. The registerCompleteCallback(...) wiring line itself is exercised by the other reconfiguration tests that go through the handler; the node-list reload path (reconfScmNodes) is covered end-to-end. Happy to add a framework-level test hook if you'd prefer the method be private, but that touches shared ReconfigurationHandler infrastructure used by the datanode and SCM too, so I left it out of this PR.
There was a problem hiding this comment.
Thanks for the explanation, that makes sense. Could we still add two cases here? One with an unrelated key, to check no reload happens, and one with a malformed address, to check the failure is caught.
There was a problem hiding this comment.
Added both: testReloadScmProxiesOnReconfigIgnoresUnrelatedKey (unrelated key → no reload, providers keep the original address) and testReloadScmProxiesOnReconfigCatchesMalformedAddress (malformed address → callback catches the failure, doesn't throw, membership unchanged). Thanks for the suggestion.
0331681 to
49651fb
Compare
chihsuan
left a comment
There was a problem hiding this comment.
Thanks for the follow-up! @hani-fouladgar I looked a bit more and left a few small notes. Also, I just noticed the two new keys are not listed in Reconfigurability.md. Should we add them?
| // IllegalArgumentException) and only log, so an address-only change that | ||
| // cannot be resolved leaves the previous proxies in place. | ||
| LOG.warn("Failed to reload SCM failover proxies after reconfiguration of {} / {}*; " | ||
| + "keeping the previous SCM proxy configuration", scmNodesKey, scmAddressPrefix, e); |
There was a problem hiding this comment.
Heads-up for the follow-up Jira rather than a change request. If an address key is unset, or set to host:port, the bad value stays in the live conf. getServiceList() then throws, so new clients can't reach this OM until fixed.
| * resolvable address (which would break {@code getServiceList()}). To add an | ||
| * SCM in a single {@code reconfig start}, set its address key together with | ||
| * the node list; the reconfiguration-complete callback | ||
| * ({@link #reloadScmProxiesOnReconfig}) applies the final membership once both |
There was a problem hiding this comment.
Could we reword this? When the node list is applied before the address, it is rolled back, so the callback reloads the old list. Adding a node then needs a second reconfig start. Same for the reloadScmProxiesOnReconfig javadoc.
There was a problem hiding this comment.
Reworded both the reconfScmNodes and reloadScmProxiesOnReconfig javadocs: if the node list is applied before the address it's rolled back, the complete callback reloads the old list, and the node is only picked up on a second reconfig start once the address is stored.
| handler.reconfigureProperty(scmNodesKey, String.join(",", remaining)); | ||
|
|
||
| Set<String> afterContainer = | ||
| new HashSet<>(scmClient.getContainerProxyProvider().getSCMNodeIds()); |
There was a problem hiding this comment.
nit: Would it be worth calling getScmInfo() after the reload here? The tests only compare node ids, so a proxy that was stopped but still in use would not be caught.
There was a problem hiding this comment.
Good call. Added a live getScmInfo() RPC through both rebuilt providers after the reload, so a proxy that was stopped but left in use is caught rather than just matching node ids.
Good catch — added |
|
@hani-fouladgar , is this AI generated/assisted? If yes, please add the AI model (ASF requirement). Please also shorten the long javadoc/comments since Javadoc/comments should be as concise as possible. |
…M node list (ozone.scm.nodes / ozone.scm.address) without a restart Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…guard stale failover target - reconfScmNodes: write the new ozone.scm.nodes value into conf before triggering the proxy reload (ReconfigurableBase stores it only after the callback returns), and roll it back if the reload fails. - changeConfig: fix up the current-proxy pointer before stopping stale proxies, clear a stale updatedLeaderNodeID, and guard RPC.stopProxy. - ScmClient: add @VisibleForTesting proxy-provider accessors. - Rewrite the reconfiguration integration test to drop a node and assert the reload takes effect on both block and container providers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…onfig-complete so address-only changes take effect, document the failover window, and strengthen tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… in complete callback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…esses Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…allback cases, doc the keys Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
15687d6 to
7e63537
Compare
I added |
szetszwo
left a comment
There was a problem hiding this comment.
@hani-fouladgar , Thanks for working on this!
I need you to review this AI generated PR first:
- Revert formatting and unnecessary changes (as an example, see the comment inlined)
- Review the comments/javadocs -- they are still too long and hard to maintain.
| } | ||
| InetSocketAddress protocolAddr = NetUtils.createSocketAddr(protocolAddress); | ||
|
|
||
| String scmServiceId = scmNodeInfo.getServiceId(); | ||
| String scmNodeId = scmNodeInfo.getNodeId(); | ||
| newScmNodeIds.add(scmNodeId); | ||
| // Preserve the original config string so DNS can be re-resolved on | ||
| // connection failure when the SCM peer is rescheduled to a new IP | ||
| // (Kubernetes pod-IP-change recovery). See refreshProxyAddressIfChanged. | ||
| SCMProxyInfo scmProxyInfo = new SCMProxyInfo(scmServiceId, scmNodeId, | ||
| protocolAddr, protocolAddress); | ||
| newScmProxyInfoMap.put(scmNodeId, scmProxyInfo); | ||
| } | ||
|
|
||
| return new ScmProxyConfig(newScmNodeIds, newScmProxyInfoMap); | ||
| } |
There was a problem hiding this comment.
The else is unnecessarily removed here and the comment is reformatted. Please revert them and minimize the change.
It took me 10 min to figure out such a simple change here.
+ private ScmProxyConfig buildConfigs() {
List<SCMNodeInfo> scmNodeInfoList = SCMNodeInfo.buildNodeInfo(conf);
- scmNodeIds = new ArrayList<>();
-
+ final List<String> newScmNodeIds = new ArrayList<>();
+ final Map<String, SCMProxyInfo> newScmProxyInfoMap = new HashMap<>();
for (SCMNodeInfo scmNodeInfo : scmNodeInfoList) {
String protocolAddress = getProtocolAddress(scmNodeInfo);
@@ -188,16 +199,96 @@ protected synchronized void loadConfigs() {
String scmServiceId = scmNodeInfo.getServiceId();
String scmNodeId = scmNodeInfo.getNodeId();
- scmNodeIds.add(scmNodeId);
+ newScmNodeIds.add(scmNodeId);
// Preserve the original config string so DNS can be re-resolved
// on connection failure when the SCM peer is rescheduled to a
// new IP (Kubernetes pod-IP-change recovery). See
// refreshProxyAddressIfChanged(String).
SCMProxyInfo scmProxyInfo = new SCMProxyInfo(scmServiceId, scmNodeId,
protocolAddr, protocolAddress);
- scmProxyInfoMap.put(scmNodeId, scmProxyInfo);
+ newScmProxyInfoMap.put(scmNodeId, scmProxyInfo);
+ }
+ }
+
+ return new ScmProxyConfig(newScmNodeIds, newScmProxyInfoMap);
+ }
There was a problem hiding this comment.
Reverted to else as requested, though it's technically unnecessary since the throw exits the method. Kept the newScmProxyInfoMap/newScmNodeIds locals because buildConfigs() must not touch shared state — it returns a fresh holder so changeConfig() can resolve addresses off-lock, and a mid-loop throw leaves the live config untouched.
Done — shortened the comments/javadocs and added the else back. |
szetszwo
left a comment
There was a problem hiding this comment.
@hani-fouladgar , thanks for the update!
Reviewing SCMFailoverProxyProviderBase; please see the comments inilned.
| currentProxyIndex = 0; | ||
| currentProxySCMNodeId = scmNodeIds.get(currentProxyIndex); | ||
| } else { | ||
| currentProxyIndex = scmNodeIds.indexOf(currentProxySCMNodeId); |
There was a problem hiding this comment.
- Call indexOf(..) first to avoid calling contains(..).
- In this else case, does it need to update currentProxySCMNodeId?
There was a problem hiding this comment.
Done, single indexOf() now. And no, the else case doesn't need to touch currentProxySCMNodeId: when indexOf() finds the node, scmNodeIds.get(index) is by definition the same ID we searched for, so only the index needs re-syncing.
| Map<String, SCMProxyInfo> oldProxyInfoMap = new HashMap<>(scmProxyInfoMap); | ||
| scmNodeIds = newConfig.nodeIds; | ||
| scmProxyInfoMap.clear(); | ||
| scmProxyInfoMap.putAll(newConfig.proxyInfoMap); |
There was a problem hiding this comment.
Could they be updated without copying the map?
Map<String, SCMProxyInfo> oldProxyInfoMap = scmProxyInfoMap;
scmProxyInfoMap = newConfig.proxyInfoMap;There was a problem hiding this comment.
Good catch — done.
| for (Map.Entry<String, ProxyInfo<T>> entry : staleProxies.entrySet()) { | ||
| try { | ||
| RPC.stopProxy(entry.getValue().proxy); | ||
| } catch (RuntimeException stopEx) { | ||
| getLogger().warn("Failed to stop stale proxy for SCM node {}", | ||
| entry.getKey(), stopEx); | ||
| } |
There was a problem hiding this comment.
Why not just stop the stale proxies in the loop above?
There was a problem hiding this comment.
RPC.stopProxy() can block while the IPC client tears down its connections, and doing that inside the synchronized block would stall every concurrent getProxy() / shouldRetry() caller on the provider monitor. Same reason we resolve DNS before locking. refreshProxyAddressIfChanged() already uses the same evict-under-lock / stop-after-unlock shape.
szetszwo
left a comment
There was a problem hiding this comment.
+1 the change looks good.
* master: (64 commits) HDDS-16716. Add description for ozone.scm.ec.pipeline.per.volume.factor (#11415) HDDS-16008. PutBlocks from Flushes also go without Raft (#11356) HDDS-16666. Flush SCM transaction in memory during apply transaction (#11409) HDDS-15749. Run specific JUnit tests if possible (#10671) HDDS-16362. GetObjectAttributes ObjectParts should return Part entries for FSO buckets (#11242). HDDS-16643. Remove CleanupTableInfo mechanism (#11365) HDDS-16721. StreamBlockInputStream.read() returns a negative value for bytes 0x80 to 0xFF (#11411) HDDS-16241. gRPC deadline kills long-lived block streams after 30 seconds and the client never recovers (#11080) HDDS-15991. Speed up deleted table scans in quota repair (#11386) HDDS-16674. Bump awssdk to 2.55.6 (#11407) HDDS-16673. Avoid redundant ListBuckets RPCs when S3 bucket listing reaches the end (#11397) HDDS-16708. Let dependabot ignore iceberg minor version upgrades (#11398) HDDS-16713. Bump develocity-maven-extension to 2.6.0 (#11405) HDDS-16300. Allow OM to dynamically reconfigure its SCM node list without a restart (#11218) HDDS-15089. Support S3 per request read consistency (#11252) HDDS-16704. ReadBlock fails with IllegalStateException when a response is shorter than responseDataSize (#11402) HDDS-16631. Fix chooseRandom for rack names with common prefixes (#11401) HDDS-16654. Replace usage of deprecated finalize() in OM (#11376) HDDS-16658. Reuse source key details when opening input stream in S3 CopyObject (#11396) HDDS-16711. Bump moment to 2.31.0 (#11373) ... Conflicts: hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/HealthyReadOnlyNodeHandler.java hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/NodeStateManager.java hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/SCMNodeManager.java hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/ha/TestSCMStateMachine.java hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/node/TestDeadNodeHandler.java hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/node/TestNodeStateManager.java hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/rpc/TestRpcClient.java hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/ha/TestHadoopRpcOMFollowerReadFailoverProxyProvider.java hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/shell/TestOzoneShellHA.java hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/upgrade/OMCancelPrepareResponse.java hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/upgrade/OMCompleteFinalizeUpgradeResponse.java hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/upgrade/OMPrepareResponse.java hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOzoneManagerStateMachine.java
What changes were proposed in this pull request?
When an SCM is added to (or removed from) an SCM-HA ring, the OM only learns the new membership from
ozone.scm.nodes.<serviceId>/ozone.scm.address.<serviceId>.<nodeId>at startup. The OM's SCM block and container failover proxy providers build their node list once in their constructor, so reaching a newly added SCM previously required restarting the OM. This makes SCM scale-out/migration disruptive for the OM.Please describe your PR in detail:
Approach
Make the OM's SCM proxy providers reloadable and drive the reload through the existing reconfiguration mechanism, so no restart is needed.
SCMFailoverProxyProviderBase.changeConfig()(new): reloads the node list and addresses from the (already-updated) configuration.loadConfigs()now builds the new node list / proxy-info map in temporaries and commits them only after the whole config parses successfully. Adding an SCM requires two properties (the node list and the new node's address); they may be applied in either order, and if the node list is updated first the reload throws and leaves the previous state intact so the operator can retry.RPC.stopProxy) so the next call dials the fresh address.HAUtils: added overloads ofgetScmBlockClient/getScmContainerClientthat accept a caller-supplied proxy provider, so the OM can keep the reference needed to reload it.ScmClient.reloadScmNodes()(new): delegates to changeConfig() on the block and container providers; a no-op when providers are absent (e.g. mock-constructed clients).OzoneManager: constructs and retains the block/container proxy providers, passes them toScmClient, and — only when an SCM service id is configured (SCM HA) — registersozone.scm.nodes.<serviceId>and, as a prefix,ozone.scm.address.<serviceId>.as reconfigurable. ThereconfScmNodescallback rejects an empty node list and otherwise triggersreloadScmNodes(). The address keys are registered as a prefix because a newly added node's key does not exist at startup and cannot be registered by name in advance.This mirrors the existing datanode-side SCM reconfiguration (HDDS-16.x,
TestDatanodeSCMNodesReconfiguration) and keeps service boundaries intact — the OM only reloads its own client proxies.What is the link to the Apache JIRA
https://issues.apache.org/jira/browse/HDDS-16300
How was this patch tested?
Testing
TestSCMFailoverProxyProviderChangeConfig(unit):changeConfig()adds a node, removes a node while keeping the current-proxy pointer valid, and fails cleanly (leaving state intact) when a referenced node's address is missing.TestScmClient(unit):reloadScmNodes()delegates to both providers; no-op when providers are null.TestOmSCMNodesReconfiguration(integration, SCM-HAMiniOzoneCluster): the SCM node list and per-node address prefix are reconfigurable on a running OM; an empty node list is rejected; a valid reconfigure drives the proxy reload end-to-end.Manual Testing
ozone.scm.nodes.scmservicefrom OM (in memory)reconfigthe OM to get the new value from the configreconfigstatusozone.scm.nodes.scmservicefrom OM (in memory)scm3backozone.scm.nodes.scmservicefrom OM (in memory)reconfigthe OM to get the new value from the configreconfigstatusozone.scm.nodes.scmservicefrom OM (in memory)curl -s http://localhost:9874/conf | tr -d '\n' | grep -oE 'ozone.scm.(address|nodes).scmservice\s*[^<]+'
ozone.scm.nodes.scmservicescm1,scm2,scm3
bash-5.1$