Siyao Meng created HDDS-16860:
---------------------------------

             Summary: OM leader terminates on CompleteFinalizeUpgrade when a 
quota repair is already running, and stays partially finalized after restart
                 Key: HDDS-16860
                 URL: https://issues.apache.org/jira/browse/HDDS-16860
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-2-skip-upgrade-quota-repair-when-already-running.patch, 
TestBugMC2QuotaRepairStuckFinalizing.java

h3. Mechanism
{{QuotaRepairUpgradeAction.execute}} runs on the OM that is the ready leader 
when it applies {{CompleteFinalizeUpgrade}} and calls 
{{QuotaRepairTask.repair}}. If a quota repair is already running on that OM, 
{{repair}} throws {{QUOTA_ERROR}} "Quota repair is already running". The action 
only catches {{OMNotLeaderException}} and {{OMLeaderNotReadyException}}, so 
{{OMVersionManager.runUpgradeAction}} wraps the error in an 
{{UpgradeException}} ({{FINALIZE_UPGRADE_ACTION_FAILED}}). That is not an 
{{OMException}}, so the response status is {{INTERNAL_ERROR}} and 
{{OzoneManagerStateMachine.processResponse}} terminates the OM.

At that point the versions before QUOTA have already been written to the 
VERSION file. The error response is also already in the double buffer, which 
skips its DB batch but still records its transaction index. The restarted OM 
therefore resumes after the failed entry and never applies it again. It stays 
at {{FILESYSTEM_SNAPSHOT}} (5) with the finalization in progress key set, while 
the followers, which skip the action, finalize to the software version.

h3. Trigger
# A cluster is pre-finalized at a layout before QUOTA, with 
{{ozone.om.upgrade.quota.recalculate.enabled}} at its default ({{true}}).
# An admin starts a quota repair ({{ozone repair om quota start}}, which runs 
on the leader).
# While it is still running, the admin finalizes the upgrade and the leader 
applies {{CompleteFinalizeUpgrade}}.

No fault is needed. On a large cluster one repair can run for a long time; the 
reproduction keeps one running by calling {{startQuotaRepair}} in a loop.

h3. Impact
* The leader OM terminates during the upgrade (run). The other OMs elect a new 
leader and keep serving.
* After a restart over the same storage, that OM has applied past 
{{CompleteFinalizeUpgrade}} but reports {{FILESYSTEM_SNAPSHOT}} in memory and 
in its VERSION file, with the finalization in progress key still in its DB, 
while its peers are at the software version (run).
* Argued, not run: a new OM process recreates {{OMUpgradeFinalizeService}} 
because it still needs finalization. That service submits 
{{CompleteFinalizeUpgrade}} only while the OM is the ready leader, so the OM 
stays behind until leadership moves to it or it installs a Ratis snapshot from 
the leader ({{RatisBasedVersionManager.finalizeFromSnapshotIfRequired}}). 
Restarting it does not help. While behind, it reports an older version to 
clients and keeps version gated features such as snapshot defrag off.
* In the reproduction the double buffer always flushed the failed entry before 
the OM was stopped. With a real process exit this is a race. If the exit wins, 
the entry is applied again on restart and is expected to finalize.
* Affects 2.0.0 onward for the termination. 
{{QuotaRepairUpgradeAction.execute}} is the same since 2.0.0, and in 2.x it 
runs while the leader applies {{FinalizeUpgrade}}, so the same error is 
expected to fail that request with {{INTERNAL_ERROR}} and terminate the leader 
(argued from code, not run). The partially finalized state after restart is 
specific to master, because {{CompleteFinalizeUpgrade}} and 
{{OMUpgradeFinalizeService}} are not in any release.

h3. Reproduction
PASS on the unmodified source. [^TestBugMC2QuotaRepairStuckFinalizing.java] 
({{hadoop-ozone/integration-test}}) runs a 3 OM HA mini cluster pre-finalized 
at {{INITIAL_VERSION}} and uses only admin RPCs ({{startQuotaRepair}} in a 
loop, then {{finalizeUpgrade}}). The mini cluster disables {{System.exit}}, so 
the test stops the terminated OM at once, as a process exit would, and restarts 
it with {{restartOzoneManager}}. The test asserts:
* the exit message names {{CompleteFinalizeUpgrade}} and "Quota repair is 
already running";
* the restarted OM has applied past that entry but is still at 
{{FILESYSTEM_SNAPSHOT}} with the marker set;
* the peers are at the software version.

A passing test means the defect is present. Recovery after a leadership change 
is not covered, because {{OzoneManager.restart}} (used by the mini cluster) 
does not restart {{OMUpgradeFinalizeService}}.

h3. Patch
[^MC-2-skip-upgrade-quota-repair-when-already-running.patch], against 
a6b7bdb937109ba2893688b41a89470bc95c88cf. It also applies to master at 
b0aa6475b78.

{{QuotaRepairUpgradeAction}} now treats {{QUOTA_ERROR}} from 
{{QuotaRepairTask.repair}} like the existing not leader case. It logs a WARN 
and lets finalization continue, and rethrows any other {{OMException}}. The 
action only starts an asynchronous repair whose result finalization never waits 
for, so a repair that is already running is a reason to skip, not to fail. The 
WARN asks the operator to run quota repair again for all buckets if the running 
one was limited with {{--buckets}}.

Covered by {{testUpgradeActionDoesNotFailWhenQuotaRepairIsRunning}} in the 
existing {{TestQuotaRepairTask}}. It holds a real repair inside its Ratis 
submit and runs the action as the ready leader. Without the change it fails 
with the {{QUOTA_ERROR}} thrown from {{QuotaRepairUpgradeAction.execute}}. With 
it, these pass:
* {{TestQuotaRepairTask}}, {{TestOMVersionManager}}, 
{{TestOMUpgradeFinalizeService}} and {{TestOMCompleteFinalizeUpgradeRequest}} 
(48 tests);
* {{TestOMUpgradeFinalization}}, {{TestOMBucketLayoutUpgrade}} and 
{{TestUpgradeFinalizationWithOldClients}} on a mini cluster (12 tests).

Checkstyle is clean. With the patch the reproduction fails at the wait for the 
OM to terminate: the leader logs the skip and all OMs finalize.

The patch removes this trigger only. Any write whose apply returns 
{{INTERNAL_ERROR}} can have its index flushed before the process exits. It is 
then not applied again after restart. For {{CompleteFinalizeUpgrade}} that now 
needs an I/O failure in an upgrade action or in the VERSION file write.

Two related fixes in {{QuotaRepairUpgradeAction}}, filed separately and linked 
from there, change the same few lines of {{QuotaRepairUpgradeAction.execute}}, 
so whichever lands later needs a trivial rebase.

Found by TLA+ model checking and code review of the OM upgrade finalization 
paths under HDDS-15926, on commit a6b7bdb937109ba2893688b41a89470bc95c88cf. 
Checked against HDDS issues and apache/ozone pull requests for duplicates 
before filing. The attached patch is a proposal for review. Generated with 
Specula (Claude Opus 5.5).



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to