Skip to content

HDDS-16300. Allow OM to dynamically reconfigure its SCM node list without a restart - #11218

Merged
szetszwo merged 12 commits into
apache:masterfrom
hani-fouladgar:HDDS-16300
Oct 5, 2026
Merged

szetszwo merged 12 commits into
apache:masterfrom
hani-fouladgar:HDDS-16300

Conversation

@hani-fouladgar

Copy link
Copy Markdown
Contributor

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.
    • Cached proxies for removed nodes, or nodes whose address changed, are stopped (RPC.stopProxy) so the next call dials the fresh address.
    • The current-proxy pointer is kept valid: it re-syncs its index to the rebuilt list, falling back to the first node if the node it referenced was removed.
  • HAUtils: added overloads of getScmBlockClient / getScmContainerClient that 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 to ScmClient, and — only when an SCM service id is configured (SCM HA) — registers ozone.scm.nodes.<serviceId> and, as a prefix, ozone.scm.address.<serviceId>. as reconfigurable. The reconfScmNodes callback rejects an empty node list and otherwise triggers reloadScmNodes(). 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-HA MiniOzoneCluster): 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

  1. Manually shrink the SCM node list 3 -> 2
bash-5.1$ sed -i 's/scm1,scm2,scm3/scm1,scm2/' /etc/hadoop/ozone-site.xmlp/ozone-site.xml
  1. Get the value of ozone.scm.nodes.scmservice from OM (in memory)
bash-5.1$ curl -s http://localhost:9874/conf | tr -d '\n' | grep -oE '<name>ozone\.scm\.(address|nodes)\.scmservice</name>\s*<value>[^<]+</value>'
<name>ozone.scm.nodes.scmservice</name><value>scm1,scm2,scm3</value>
  1. Trigger reconfig the OM to get the new value from the config
bash-5.1$ ozone admin reconfig --service OM --address om1:9862 start   
OM: Started reconfiguration task on node [om1:9862].
  1. Check the reconfig status
bash-5.1$ ozone admin reconfig --service OM --address om1:9862 status 2>&1 | grep -vE "TracingUtil|Sampling" 
OM: Reconfiguring status for node [om1:9862]: started at Thu Sep 03 20:53:38 UTC 2026 and finished at Thu Sep 03 20:53:38 UTC 2026.
SUCCESS: Changed property ozone.scm.nodes.scmservice
	From: "scm1,scm2,scm3"
	To: "scm1,scm2"
  1. Get the value of ozone.scm.nodes.scmservice from OM (in memory)
bash-5.1$ curl -s http://localhost:9874/conf | tr -d '\n' | grep -oE '<name>ozone\.scm\.(address|nodes)\.scmservice</name>\s*<value>[^<]+</value>'
<name>ozone.scm.nodes.scmservice</name><value>scm1,scm2</value>
  1. Add scm3 back
bash-5.1$ sed -i 's/scm1,scm2/scm1,scm2,scm3/' /etc/hadoop/ozone-site.xml
  1. Get the value of ozone.scm.nodes.scmservice from OM (in memory)
bash-5.1$ curl -s http://localhost:9874/conf | tr -d '\n' | grep -oE '<name>ozone\.scm\.(address|nodes)\.scmservice</name>\s*<value>[^<]+</value>'
<name>ozone.scm.nodes.scmservice</name><value>scm1,scm2</value>
  1. Trigger reconfig the OM to get the new value from the config
bash-5.1$ ozone admin reconfig --service OM --address om1:9862 start 
OM: Started reconfiguration task on node [om1:9862].
  1. Check the reconfig status
bash-5.1$ ozone admin reconfig --service OM --address om1:9862 status 2>&1 | grep -vE "TracingUtil|Sampling" 
OM: Reconfiguring status for node [om1:9862]: started at Thu Sep 03 21:01:59 UTC 2026 and finished at Thu Sep 03 21:01:59 UTC 2026.
SUCCESS: Changed property ozone.scm.nodes.scmservice
	From: "scm1,scm2"
	To: "scm1,scm2,scm3"
  1. Get the value of ozone.scm.nodes.scmservice from 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$

@aryangupta1998
aryangupta1998 requested review from ivandika3 and szetszwo and removed request for ivandika3 September 10, 2026 19:24
@hani-fouladgar hani-fouladgar changed the title HDDS-16300: Allow the Ozone Manager to dynamically reconfigure its SCM node list (ozone.scm.nodes / ozone.scm.address) without a restart HDDS-16300. Allow the Ozone Manager to dynamically reconfigure its SCM node list (ozone.scm.nodes / ozone.scm.address) without a restart Sep 10, 2026

@chihsuan chihsuan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Removed the duplicated "applied in either order" reasoning here; it now lives in one place — the reconfScmNodes javadoc in OzoneManager.

@hani-fouladgar

hani-fouladgar commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor Author

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?

Added to the PR description.

@chihsuan chihsuan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we catch and log here? If the reload throws, the remaining complete callbacks (tracing, logging) are skipped.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done!


Map<String, Boolean> changed = new HashMap<>();
changed.put(scmAddrKey, true);
om.reloadScmProxiesOnReconfig(changed, conf);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we also cover the nodes-before-address case? reconfigureProperty never fires the complete callback, so its wiring is not exercised by these tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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:

  1. Node list applied first (address not set yet) → the property fails with ReconfigurationException and rolls back → asserts the node is not added.
  2. The new SCM's address applied (prefix key, no reload of its own).
  3. Node list reapplied → now resolves → asserts the node is added on both providers.

@chihsuan chihsuan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Could we import SCMFailoverProxyProviderBase here instead of the fully qualified name?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@adoroszlai adoroszlai changed the title HDDS-16300. Allow the Ozone Manager to dynamically reconfigure its SCM node list (ozone.scm.nodes / ozone.scm.address) without a restart HDDS-16300. Allow OM to dynamically reconfigure its SCM node list without a restart Sep 19, 2026
@hani-fouladgar

Copy link
Copy Markdown
Contributor Author

Adding an SCM can fail if the node list is applied before it

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:

  • node-list-before-address needs the address set first (or a retry) — same two-pass behavior as the datanode, which skips an unresolvable add;
  • an unresolvable address-only change is logged and the previous proxies are kept, but the status still reads SUCCESS because a reconfiguration-complete callback can't fail the batch.

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 chihsuan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done!

scmClient.reloadScmNodes();
LOG.info("Reloaded SCM failover proxies after reconfiguration of {} / {}*",
scmNodesKey, scmAddressPrefix);
} catch (ConfigurationException e) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here, should this catch RuntimeException too? A malformed address-only change would still escape and skip the tracing and logging callbacks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done!


Map<String, Boolean> changed = new HashMap<>();
changed.put(scmAddrKey, true);
om.reloadScmProxiesOnReconfig(changed, conf);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@chihsuan chihsuan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@hani-fouladgar hani-fouladgar Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@hani-fouladgar

Copy link
Copy Markdown
Contributor Author

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?

Good catch — added ozone.scm.nodes.<scmServiceId> and ozone.scm.address.<scmServiceId>.<scmNodeId> to the OM table in Reconfigurability.md.

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 23, 2026
@szetszwo

szetszwo commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

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

hani-fouladgar and others added 6 commits September 24, 2026 11:53
…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>
hani-fouladgar and others added 2 commits September 24, 2026 11:53
…allback cases, doc the keys

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hani-fouladgar

Copy link
Copy Markdown
Contributor Author

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

I added Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> to commits' comments. I also shortened the javadoc/comments as much as I could.

@szetszwo szetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment on lines +198 to +213
}
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);
}

@szetszwo szetszwo Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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);
+  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@hani-fouladgar

Copy link
Copy Markdown
Contributor Author
  • comments/javadocs

Done — shortened the comments/javadocs and added the else back.

@szetszwo szetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@hani-fouladgar , thanks for the update!

Reviewing SCMFailoverProxyProviderBase; please see the comments inilned.

currentProxyIndex = 0;
currentProxySCMNodeId = scmNodeIds.get(currentProxyIndex);
} else {
currentProxyIndex = scmNodeIds.indexOf(currentProxySCMNodeId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  • Call indexOf(..) first to avoid calling contains(..).
  • In this else case, does it need to update currentProxySCMNodeId?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment on lines +227 to +230
Map<String, SCMProxyInfo> oldProxyInfoMap = new HashMap<>(scmProxyInfoMap);
scmNodeIds = newConfig.nodeIds;
scmProxyInfoMap.clear();
scmProxyInfoMap.putAll(newConfig.proxyInfoMap);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could they be updated without copying the map?

Map<String, SCMProxyInfo> oldProxyInfoMap = scmProxyInfoMap;
scmProxyInfoMap = newConfig.proxyInfoMap;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — done.

Comment on lines +260 to 266
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not just stop the stale proxies in the loop above?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 szetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1 the change looks good.

@szetszwo
szetszwo merged commit ec64e92 into apache:master Oct 5, 2026
45 checks passed
errose28 added a commit that referenced this pull request Oct 6, 2026
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation om

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants