Skip to content

Add deleteall -c to delete only the children of a znode - #159

Merged
laxman-ch merged 8 commits into
branch-3.6from
vchekka/deleteall-keep-root
Oct 22, 2026
Merged

laxman-ch merged 8 commits into
branch-3.6from
vchekka/deleteall-keep-root

Conversation

@laxman-ch

@laxman-ch laxman-ch commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Add a -c option to the deleteall CLI command that deletes all descendants of a znode but keeps the znode itself: deleteall <path> [-b batch size] [-c].

Why: Clearing a parent without deleting it is a frequent operational need (for example, draining a Helix MESSAGES znode that accumulated hundreds of thousands of children). deleteall always removes the parent too, and it lists the subtree with a plain getChildren, which fails with Packet len N is out of range! when the children list exceeds jute.maxbuffer.

How:

  • New ZKUtil.deleteChildrenRecursive(zk, path, batchSize): lists the subtree with getAllChildrenPaginated, then deletes everything except the root, leaves first, using the existing batched/sync delete logic. A childless node is a no-op returning true. Because the root is kept, it checks the node's numChildren after deleting and returns false if any child is left (for example one created concurrently).
  • -c is rejected (IllegalArgumentException, surfaced as a malformed-path error by the CLI) for / on a client without a chroot. The root can never be emptied, because its children include the protected /zookeeper subtree that the server refuses to delete, so this restriction is permanent and must not be relaxed. Separately, server-side paginated listing of / is broken on servers without the root-pagination fix (Fix paginated getChildren returning no children for the root path #160), which would make the attempt look like it succeeded on an empty tree. A chrooted / is supported.
  • New package-private ZKUtil.listSubTreeBFS(zk, path, paginated) overload. The existing two-argument public method delegates with paginated=false, so it still calls getChildren. The overload is deliberately not public: with paginated=true on an unchrooted / it can return an incomplete subtree without raising, and only deleteChildrenRecursive guards that case.
  • deleteInBatch gets a lastIndex parameter (0 = include root, 1 = skip root). Existing callers pass 0, which is identical to the previous behavior.
  • DeleteAllCommand: -c calls deleteChildrenRecursive; without -c it calls deleteRecursive exactly as before. A false result on the -c path now reports that children remain rather than that a delete failed, since the node itself is kept and a concurrent create is the usual cause.
  • Docs updated in zookeeperCLI.md.
deleteall /ESPRESSO/INSTANCES/host_1234/MESSAGES -c -b 1000

Operational notes

  • Client jute.maxbuffer must be at least the server's. Every page is requested with maxReturned=Integer.MAX_VALUE, so the page size is bounded only by the server's jute.maxbuffer, while the client independently rejects an oversized incoming packet. If the server's limit is the larger of the two, the caller still gets Packet len N is out of range! — the error this option exists to avoid. Run zkCli with a matching -Djute.maxbuffer.
  • Quiesce writers first. The subtree is listed before it is deleted, which takes a while on a large node. A child deleted in between makes its batch fail as a unit and aborts the command, and a child created in between makes the final check return false. Re-running is safe and converges.

Tests

The following tests are written for this issue:

ZKUtilTest

  • testDeleteChildrenRecursiveKeepsRootInAsyncMode
  • testDeleteChildrenRecursiveKeepsRootInSyncMode
  • testDeleteChildrenRecursiveOnChildlessRoot
  • testDeleteChildrenRecursiveBeyondJuteMaxBuffer
  • testDeleteChildrenRecursiveBatchBoundaries
  • testDeleteChildrenRecursiveDoesNotTouchSiblingsOrParent
  • testDeleteChildrenRecursiveOnMissingNode
  • testDeleteChildrenRecursiveInvalidPath
  • testListSubTreeBFSPaginatedMatchesDefault
  • testListSubTreeBFSPaginatedFromRootBuildsValidPaths
  • testDeleteRecursiveStillDeletesRoot
  • testDeleteChildrenRecursiveRejectsUnchrootedRoot
  • testDeleteChildrenRecursiveOnChrootedRoot
  • testDeleteChildrenRecursiveReportsChildCreatedConcurrently

ZooKeeperTest

  • testDeleteChildrenOnlyCli
  • testDeleteChildrenOnlyCliOnMissingNode
  • testDeleteAllWithoutChildrenOptionStillDeletesNode
  • testDeleteChildrenOnlyCliRejectsRoot
  • testDeleteAllRootWithoutChildrenOptionIsUnchanged

The following is the result of the mvn test command on the appropriate module (zookeeper-server, JDK 11):

mvn -o test -pl zookeeper-server -Dtest='ZKUtilTest,ZooKeeperTest,GetChildrenPaginatedTest'
Tests run: 20, Failures: 0, Errors: 0, Skipped: 0 - in org.apache.zookeeper.ZKUtilTest
Tests run: 10, Failures: 0, Errors: 0, Skipped: 0 - in org.apache.zookeeper.test.GetChildrenPaginatedTest
Tests run: 43, Failures: 0, Errors: 0, Skipped: 0 - in org.apache.zookeeper.ZooKeeperTest
Tests run: 73, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Also passing: -Pfull-build apache-rat:check verify -DskipTests spotbugs:check checkstyle:check on zookeeper-server.

  • Local code review completed

Self-reviewed by three independent reviewers (Claude Opus 5.5, GPT-5.5, GPT-6 Sol). Two reviews found issues (handling of / as the target path, the root not being re-checked after -c, a docs example error, wrong minimum server version), all fixed in the second commit. A later PR review comment (deleteall / -c could silently succeed on a large unchrooted root) was validated with a repro and by two further model reviews (Claude Opus 5.5, GPT-6.1 Sol), and fixed in the third commit.

A final self-review pass (Claude Opus 5.5 and GPT-6.1 Sol, each asked to re-derive every claim from source) confirmed that nothing in the existing behavior changes, and produced the fourth and fifth commits: narrowing the new listSubTreeBFS overload to package-private so the new public API surface is unchanged, replacing the misleading -c failure message, and correcting three documentation claims that were overstated (the jute.maxbuffer guarantee, the justification for the root restriction, and the claim that a concurrent create could never be reported as success).

These reviews ran outside the branch-review receipt flow, so the box is left unchecked.

Changes that Break Backward Compatibility (Optional)

None. ZKUtil.deleteRecursive, ZKUtil.listSubTreeBFS(zk, path) and deleteall without -c behave exactly as before (same getChildren call, same delete order and batching). The new three-argument listSubTreeBFS overload is package-private, so the public API surface is unchanged apart from the new deleteChildrenRecursive method. -c was previously rejected as an unrecognized option, and the deleteall usage string now includes [-c].

-c requires a server that supports paginated getChildren (fork 3.6.3-1 or later). Against an older server the command fails with Unimplemented before deleting anything.

Documentation (Optional)

Updated zookeeper-docs/src/main/resources/markdown/zookeeperCLI.md (deleteall usage, a -c / -b example, and the / restriction).

🤖 Generated with GitHub Copilot CLI

@arkmish

arkmish commented Oct 12, 2026

Copy link
Copy Markdown

For an unchrooted connection, deleteall / -c can return success without deleting anything when / requires pagination. The existing DataTree.buildChildrenPathWithStat() constructs //child lookups, so the server returns an empty list and deleteChildrenRecursive() immediately returns true. Reproduced with 29,129 children unchanged. The client-side slash fix does not cover this server-side lookup. Could we fix or explicitly reject this case and add a large-root regression test?

@laxman-ch

Copy link
Copy Markdown
Collaborator Author

Thanks for catching this. Confirmed and fixed client-side in 12f3891:

  • deleteall / -c is now rejected on a client without a chroot (a chrooted / still works). The protected /zookeeper subtree can never be deleted, and server-side paginated listing of / is unreliable.
  • The final emptiness check now uses the parent's numChildren instead of another listing, so a concurrent create or an incomplete listing is no longer reported as success.
  • Tests added: unchrooted / rejected without deleting anything, chrooted / cleared with the chroot node kept, a child created concurrently returns false, and CLI tests with and without -c.

The server-side //child lookup in DataTree.buildChildrenPathWithStat (present since 3.6.3-1) is a separate bug and I'll follow up on it in its own PR.

@laxman-ch

Copy link
Copy Markdown
Collaborator Author

Follow-up for the server-side root listing bug (//child lookup in DataTree): #160. This PR does not depend on it; the client guard stays so it is safe against already-deployed servers.

arkmish
arkmish previously approved these changes Oct 14, 2026

@arkmish arkmish left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM

Comment thread zookeeper-server/src/main/java/org/apache/zookeeper/ZKUtil.java
@laxman-ch

laxman-ch commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator Author

Confirmed — the subtree is fully materialized before the first delete, and getChildrenRemainingPages holds the Set and the List at the same time.

One number that makes it concrete: zkCli defaults to -Xmx256m (ZK_CLIENT_HEAP in bin/zkEnv.sh). ~142k children is roughly 45 MB, comfortable; ~500k is where it starts to bite.

The documented usage guideline is under 10k children per parent, so 500k–1M is well outside what the tool is likely to meet. Rather than restructure around getChildrenIterator, 9833f4c logs a warning above 100k naming ZK_CLIENT_HEAP, so the heap can be raised up front instead of the limit showing up as an OutOfMemoryError. The failure is a loud client-side crash and -c is idempotent, so retrying after raising the heap is safe.

Can look at streaming separately if you think it is worth doing.

@bellatrix007 bellatrix007 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Overall LGTM. The logic checks out (batch boundaries, chroot guard, numChildren verification), and the unchrooted-/ guard is well justified given #160 isn't deployed everywhere yet. The inline notes are mostly FYI or non-blocking.

Two of them (ZOOKEEPER-4325 and ZOOKEEPER-4710) are upstream fixes we'd pick up when moving to 3.8.x. 4325 is in 3.8.0+ and 4710 is in 3.8.4+ (JIRA lists only 3.9.0, but the 3.8.4 tag has the backport). Neither is in 3.6.x or 3.7.x releases.

Follow-up: #160 mentions li-dev/base-3.8.4 has the same root-listing bug. Worth tracking the port of both #159 and #160 there.

Comment thread zookeeper-server/src/main/java/org/apache/zookeeper/ZKUtil.java
Comment thread zookeeper-server/src/main/java/org/apache/zookeeper/ZKUtil.java
Comment thread zookeeper-server/src/main/java/org/apache/zookeeper/ZKUtil.java
Comment thread zookeeper-server/src/main/java/org/apache/zookeeper/cli/DeleteAllCommand.java Outdated
Comment thread zookeeper-server/src/test/java/org/apache/zookeeper/ZKUtilTest.java
Comment thread zookeeper-server/src/test/java/org/apache/zookeeper/ZKUtilTest.java
Comment thread zookeeper-server/src/main/java/org/apache/zookeeper/ZKUtil.java Outdated
@laxman-ch
laxman-ch force-pushed the vchekka/deleteall-keep-root branch from 0888d45 to d4133c1 Compare October 22, 2026 02:57
@laxman-ch

laxman-ch commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Rebased onto current branch-3.6 and pushed the review fixes.

One correction on the follow-up: li-dev/base-3.8.4 already has ZOOKEEPER-4325, so the client-side root listing is fine there. It does carry the #160 server-side bug — DataTree.buildChildrenPathWithStat still uses path + "/" + child — so #160 is worth porting. #159 would not port as-is, since that branch has no paginated listing in ZKUtil.

The earlier red full-build jobs were never related to this change. GitHub began migrating the ubuntu-latest label from 24.04 to 26.04, where the C client does not compile (strchr is const-generic, and the build uses -Werror) and the cppunit SSL tests fail against the newer OpenSSL. That blocked every PR against branch-3.6.

Fixed in #169, now merged, which pins the runners to ubuntu-24.04. Rebased onto that (f736f820) and re-pushed; this PR carries only the five Java/doc files, no workflow or C changes of its own.

laxman-ch and others added 8 commits October 22, 2026 11:06
deleteall -c keeps the target znode and deletes only its descendants, listing
the subtree with getAllChildrenPaginated so it works on znodes whose children
list exceeds jute.maxbuffer. Existing deleteall/ZKUtil.deleteRecursive/
listSubTreeBFS behavior is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…docs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hild count

Paginated listing of '/' on current servers can silently return no children, and
the protected /zookeeper subtree can never be deleted, so 'deleteall / -c' either
reported success without deleting anything or partially deleted data before
failing. Reject it unless the client has a chroot, and replace the final
emptiness check with the parent's numChildren so neither a concurrent create nor
an incomplete listing is reported as success.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…re message

Two follow-ups from self-review:

- The new listSubTreeBFS(ZooKeeper, String, boolean) overload was public.
  Called with paginated=true on the unchrooted root it returns just the root
  with no exception when the root children do not fit in a single page,
  because only deleteChildrenRecursive rejects that path. Making it
  package-private keeps the new public API surface unchanged and avoids
  shipping an API with a silent wrong-answer mode. All callers are in
  org.apache.zookeeper, so nothing else changes.

- "Failed to delete some node(s) in the subtree!" is misleading for -c: the
  node itself is kept, so a false result usually means a writer created new
  children while the delete was running, not that a delete failed. The -c
  path now reports that children remain and names both possible causes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- "works on nodes whose children list does not fit in jute.maxbuffer" was
  overstated. Every page is requested with maxReturned=Integer.MAX_VALUE, so
  page size is bounded only by the server's jute.maxbuffer while the client
  independently rejects oversized incoming packets. If the server's limit is
  larger than the client's, the caller still gets "Packet len out of range".
  The client requirement is now stated explicitly.

- The root rejection was justified partly by paginated listing of "/" being
  "not reliable on current servers", which stops being true once the server
  side is fixed. That wording invites someone to later remove the guard, which
  would be unsafe: the root can never be emptied because of the protected
  /zookeeper subtree. The permanent reason now leads, and the server bug is
  described as applying to servers without the root-pagination fix.

- "neither a concurrent create nor an incomplete listing can be reported as
  success" was too absolute. exists() is a point-in-time read of the contacted
  server, not a lock, so a create racing the check can still be missed.

Docs only; no behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… for deleteall -c

Mirrors the javadoc correction in the CLI reference: pages are bounded by the
server's jute.maxbuffer while the client enforces its own limit on the incoming
packet, so the client must be configured with at least the server's value.
Also notes that writers should be quiesced, since concurrent creates or deletes
can abort the command or make it report remaining children.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
deleteChildrenRecursive lists the whole subtree into client memory before
issuing the first delete, so the listing has to fit in the client's heap.
zkCli defaults to -Xmx256m (ZK_CLIENT_HEAP in bin/zkEnv.sh).

Log a warning when the target has more direct children than the threshold so
an operator can raise the heap up front instead of discovering the limit as
an OutOfMemoryError. The threshold sits an order of magnitude above the
documented guideline of under 10,000 children per parent, so it only fires
for nodes already well outside it.

Behaviour is otherwise unchanged: one extra exists() call, no change to the
traversal or the delete path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- deleteall -c now throws a CliException when children remain, so zkCli
  exits non-zero. ZooKeeperMain treats exec()'s return value as the watch
  flag, not success, and only sets a failing exit code when a CliException
  escapes, so the previous err.println left scripts unable to detect the
  failure. Plain deleteall is unchanged. Covered by a new CLI test.
- Rename deleteInBatch's lastIndex to minIndex. The loop runs backwards, so
  the parameter is the lowest index visited and the old name read both ways.
- Note the per-node round trip cost next to the client heap caveat.
- Gate the concurrent-create test stub on the second exists() call. The
  pre-listing warning check made it fire twice, so the test passed only
  because the first child was drained by the listing and re-created.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@laxman-ch
laxman-ch force-pushed the vchekka/deleteall-keep-root branch from d4133c1 to 735eb83 Compare October 22, 2026 05:38
@laxman-ch
laxman-ch merged commit 649874e into branch-3.6 Oct 22, 2026
20 checks passed
@laxman-ch
laxman-ch deleted the vchekka/deleteall-keep-root branch October 22, 2026 07:23
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.

4 participants