Skip to content

[KYUUBI #7838] Fix concurrent ZooKeeper service deregistration - #7840

Open
zhangxinyao88 wants to merge 4 commits into
apache:masterfrom
zhangxinyao88:kyuubi-7838-ci-diagnostics
Open

zhangxinyao88 wants to merge 4 commits into
apache:masterfrom
zhangxinyao88:kyuubi-7838-ci-diagnostics

Conversation

@zhangxinyao88

@zhangxinyao88 zhangxinyao88 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Why are the changes needed?

Related to #7838. In the Spark 4.3 CI run, explicit shutdown raced with ZooKeeper's deletion watcher. The watcher hit a null pointer exception in deregisterService() before signaling graceful shutdown, leaving the server waiting.

Take and clear the service node under a lock, then close it outside the lock. This lets the watcher continue graceful shutdown if another caller already took the node. A queued data-change event skips rearming when the node is gone.

How was this patch tested?

On Linux/x86 with JDK 25, the new regression test observed two close calls on the baseline and one with the fix. All 12 tests in EmbeddedZookeeperDiscoveryClientSuite passed with the fix. Spotless and Scalastyle passed.

Was this patch assisted by generative AI tooling?

Assisted-by: Codex (GPT-6)

Signed-off-by: Xinyao Zhang <43081360+zhangxinyao88@users.noreply.github.com>
@github-actions github-actions Bot added kind:infra license, community building, project builds, asf infra related, etc. module:server labels Nov 4, 2026
@wangzhigang1999

Copy link
Copy Markdown
Contributor

Thanks for adding these diagnostics! I checked the JDK25/Spark4.3 artifacts and found a useful clue.

After DescribeSessionSuite passes, the ZooKeeper NodeDeleted watcher throws an NPE in ZookeeperDiscoveryClient.deregisterService():248. It appears that the shutdown thread and watcher race on serviceNode: one clears it between the other’s null check and close() call.

The exception prevents the watcher from reaching stopGracefully(), leaving gracefulShutdownLatch unreleased. Both thread dumps show the ScalaTest main thread waiting in KyuubiServiceDiscovery.stop():35, with Maven waiting for the test JVM to exit. This explains the hang in this run.

Would you be interested in following up with a fix and a regression test for this race? Thanks again for helping capture the evidence!

@zhangxinyao88 zhangxinyao88 changed the title [KYUUBI #7838][CI] Capture JVM thread dumps when Spark CI stalls [KYUUBI #7838] Fix concurrent ZooKeeper service deregistration Nov 4, 2026
@wangzhigang1999
wangzhigang1999 requested a max review from Copilot November 4, 2026 15:29
@github-actions github-actions Bot added module:ha and removed kind:infra license, community building, project builds, asf infra related, etc. module:server labels Nov 4, 2026

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

🟡 Changes recommended

A queued data-change callback can drop the deletion watch during deregistration, leaving graceful shutdown blocked indefinitely in an ordering the current test does not cover.

1 open issue
What changed in this PR

This PR targets #7838’s shutdown hang by coordinating concurrent ZooKeeper service deregistration in Kyuubi’s service-discovery layer.

Changes:

  • Atomically take and clear the service node, then close it outside the lock.
  • Skip watcher rearming when the node or its path is unavailable.
  • Add a deterministic regression test for explicit deregistration racing a deletion callback.
File Description
kyuubi-ha/​src/​test/​scala/​org/​apache/​kyuubi/​ha/​client/​zookeeper/​ZookeeperDiscoveryClientSuite.scala Adds concurrent deregistration coverage, checking single-close behavior and graceful-shutdown signaling.
kyuubi-ha/​src/​main/​scala/​org/​apache/​kyuubi/​ha/​client/​zookeeper/​ZookeeperDiscoveryClient.scala Synchronizes service-node ownership and guards watcher rearming after deregistration.

Review effort: Max

throw new KyuubiException(s"Unable to create znode for this Kyuubi " +
s"instance[${watcher.instance}] on ZooKeeper.")
// A queued NodeDataChanged event can arrive after deregistration.
Option(serviceNode).flatMap(node => Option(node.getActualPath)).foreach { path =>

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 in 1c7124f. Explicit shutdown now waits for active sessions without relying on the ZooKeeper deletion callback. The queued-callback regression passed in the JDK 25 / Spark 4.3 fork run.

@zhangxinyao88

Copy link
Copy Markdown
Contributor Author

Would you be interested in following up with a fix and a regression test for this race?

Yes, both are in this PR. I also added a regression for the case where the ZooKeeper callback is still queued during shutdown. On 1c7124f, the new regression passed and DescribeSessionSuite completed shutdown in the JDK 25 / Spark 4.3 fork run.

Original comment

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

LGTM. The fix addresses the concurrent deregistration race and removes shutdown’s dependency on the deletion callback. Regression tests and ACK fault-injection checks passed.

@wangzhigang1999

Copy link
Copy Markdown
Contributor

As a non-blocking follow-up, session draining still blocks the ZooKeeper event thread or the shared connection checker, and concurrent shutdown paths could use better coordination. Would you be interested in opening a separate issue and working on these lifecycle improvements?

val instance = serviceDiscovery.fe.connectionUrl
watcher = new DeRegisterWatcher(instance, serviceDiscovery)
serviceNode = createPersistentNode(
val node = createPersistentNode(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It seems like a simpler approach might be to make registerService and deregisterService mutually exclusive; otherwise, a situation might still arise where a node has already been deleted but a watch is still creating.

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.

I reproduced this by calling registerService() and deregisterService() concurrently. Deregistration returned before registration stored the node, so the node stayed registered. A lock would cover that case. I haven’t reproduced it through normal service startup and shutdown. Would it make sense to track this as a separate follow-up?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think holding a lock for the entire method might be the simplest approach. It could fix many concurrency issues like this while keeping the code simple.

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.

My initial concern was holding the lock during the network I/O in node.close(). I tested this yesterday with network delays and connection loss and didn’t observe any additional shutdown issues, so I’m fine with synchronizing both methods.

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.

Thanks for testing this with delays and connection loss. Do you see locking both methods as a blocker for merging #7840, or would it be okay to track it as a separate follow-up?

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants