Skip to content

HDDS-15826. Recon: add cycle guard to NSSummary /du tree walks#10723

Draft
smengcl wants to merge 3 commits into
apache:masterfrom
smengcl:HDDS-15826
Draft

HDDS-15826. Recon: add cycle guard to NSSummary /du tree walks#10723
smengcl wants to merge 3 commits into
apache:masterfrom
smengcl:HDDS-15826

Conversation

@smengcl

@smengcl smengcl commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Generated-by: Claude Code (Opus 4.8)

What is the problem

https://issues.apache.org/jira/browse/HDDS-15826

Recon serves disk-usage (/du), file-size-distribution, and directory-count views by walking the NSSummary directory tree. Three code paths do this walk recursively: EntityHandler.getTotalFileSizeDist, EntityHandler.getTotalDirCount, and ReconUtils.gatherSubPaths (used when gathering open-file subpaths).

Each recurses over NSSummary.getChildDir() with no protection against a malformed tree. If the persisted NSSummary data is corrupted so that a directory lists itself, or one of its ancestors, as a child, the child set forms a cycle. The recursion then never terminates and Recon dies with a StackOverflowError. On the read path this surfaces as an HTTP 500 from /du and, in the worst case, takes the Recon process down. Because the corruption lives in persisted state, the failure repeats on every request until the underlying data is rebuilt. The recursion also had no upper bound on depth even for a non-cyclic but pathologically deep tree.

What this changes

Convert all three walks from recursion to iterative traversal backed by an explicit stack and a visited set. Each object is visited, and counted, at most once, so a self-referencing or cyclic child can no longer drive infinite recursion or inflate the result. For a well-formed tree the results are identical to before.

Two smaller improvements ride along in the same paths:

  1. gatherSubPaths previously fetched each node's NSSummary twice (once to null-check a child before descending, once when the child was itself expanded). It now fetches once, on pop, which halves the number of getNSSummary lookups on large trees.
  2. De-duplicating a corrupted tree silently would hide a real data problem from an observability tool. Each walk now emits a single WARN per request, with the path or object context, the first time it detects a repeated reference, so operators can see that NSSummary data is corrupted rather than just receiving a quietly wrong number.

What this does not change

This is read-side hardening only. It does not attempt to prevent the corruption at write time, and it does not add response paging or a size limit to /du. Bounding the /du response for very large (but well-formed) buckets is a separate concern and is left as a follow-up.

How it was tested

  • New unit test TestEntityHandlerCycleGuard covers a self-referencing directory, a multi-node cycle with a back edge and a self loop, and a clean tree (to confirm unchanged counts) for both EntityHandler walks. Each case runs under a timeout so a regression back to infinite recursion fails fast.
  • TestReconUtils gains a cyclic-tree case for gatherSubPaths.
  • All new tests use mocks, so they add negligible CI time.
  • Existing TestNSSummaryEndpointWithFSO (28 tests) passes unchanged, confirming clean-tree behavior is preserved end to end.
  • checkstyle.sh for the recon module is clean.

The disk-usage read path walks the NSSummary directory tree recursively
in three places: EntityHandler.getTotalFileSizeDist, getTotalDirCount,
and ReconUtils.gatherSubPaths. None guard against a corrupted tree in
which a directory lists itself (or an ancestor) as a child, so such a
cycle drives unbounded recursion and crashes Recon with a
StackOverflowError (HTTP 500 on /du).

Convert all three walks to iterative traversal with a visited-set cycle
guard. Each directory is now visited and counted at most once, so a
self-referencing or cyclic child terminates the walk with a sane result
instead of overflowing the stack. Clean-tree results are unchanged.
gatherSubPaths now fetches each node once (on pop) rather than twice.

Because de-duplicating silently would hide the corruption from an
observability tool, each walk logs a WARN (once per request, with the
path/object context) the first time a repeated reference is detected.

Tests: TestEntityHandlerCycleGuard covers self-referencing, cyclic, and
clean trees for the two EntityHandler walks; TestReconUtils gains a
cyclic-tree case for gatherSubPaths. All use mocks with a timeout, so
they add negligible CI time.

Follow-up (not in this commit): add response paging/limit to the /du
endpoint to bound the response for very large buckets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 21:45

Copilot AI 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.

Pull request overview

This PR hardens Recon’s NSSummary tree-walk logic (used by /du, file-size distribution, directory counts, and open-file subpath gathering) against corrupted or pathologically deep directory graphs by replacing recursive traversal with an iterative walk that includes a cycle guard.

Changes:

  • Converted NSSummary recursive traversals to iterative stack-based traversals with a visited set in EntityHandler and ReconUtils.gatherSubPaths.
  • Added WARN-once logging when repeated references are detected during a walk, surfacing potential NSSummary corruption without crashing Recon.
  • Added targeted unit tests to ensure cyclic/self-referential NSSummary graphs terminate and produce de-duplicated results.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/handlers/EntityHandler.java Replaces recursive dir-count and file-size-distribution walks with iterative traversal + visited-set cycle guard and WARN-once logging.
hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconUtils.java Updates gatherSubPaths to an iterative traversal, reducing redundant getNSSummary lookups and preventing stack overflow on cycles.
hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconUtils.java Adds a timeout-guarded unit test covering gatherSubPaths behavior on a cyclic NSSummary graph.
hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/handlers/TestEntityHandlerCycleGuard.java New unit tests validating EntityHandler walks terminate and de-duplicate correctly on self-loops and multi-node cycles.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@smengcl smengcl requested a review from devmadhuu July 10, 2026 21:56
Review follow-up: make logNSSummaryCycle a void method and guard the
warn-once at the call site, so both getTotalFileSizeDist and
getTotalDirCount use the same "else if (!cycleLogged) { ...; cycleLogged
= true; }" idiom that gatherSubPaths already uses. Removes the constant
boolean return and the alreadyLogged parameter. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 22:15

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Address review feedback: the cyclic-tree test only checked subPaths size
and de-duplication, which would still pass if the walk wrongly included
the traversal root or dropped an expected child. Assert the exact
"/volumeId/bucketId/objectId" subpaths (in any order) so the cycle-guard
behavior is regression-proof. No production change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 22:23

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants