Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class KyuubiServiceDiscovery(
if (!isServerLost.get()) {
discoveryClient.deregisterService()
discoveryClient.closeClient()
gracefulShutdownLatch.await() // wait for graceful shutdown triggered by watcher
// Closing the client can prevent delivery of the deletion notification.
waitUntilNoActiveSessions()
} else {
warn(s"The Zookeeper ensemble is LOST")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

package org.apache.kyuubi.ha.client

import java.util.concurrent.{CountDownLatch, TimeUnit}
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean

import org.apache.kyuubi.Logging
Expand All @@ -35,7 +35,6 @@ abstract class ServiceDiscovery(
name: String,
val fe: FrontendService) extends AbstractService(name) {

protected val gracefulShutdownLatch = new CountDownLatch(1)
protected val isServerLost = new AtomicBoolean(false)

/**
Expand Down Expand Up @@ -64,16 +63,18 @@ abstract class ServiceDiscovery(
super.start()
}

// stop the server genteelly
def stopGracefully(isLost: Boolean = false): Unit = {
final protected def waitUntilNoActiveSessions(): Unit = {
var activeSessionCount = fe.be.sessionManager.getActiveUserSessionCount
while (activeSessionCount > 0) {
info(s"$activeSessionCount connection(s) are active, delay shutdown")
Thread.sleep(TimeUnit.SECONDS.toMillis(10))
activeSessionCount = fe.be.sessionManager.getActiveUserSessionCount
}
}

def stopGracefully(isLost: Boolean = false): Unit = {
waitUntilNoActiveSessions()
isServerLost.set(isLost)
gracefulShutdownLatch.countDown()
fe.serverable.stop()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,27 +230,33 @@ class ZookeeperDiscoveryClient(conf: KyuubiConf) extends DiscoveryClient {
external: Boolean = false): Unit = {
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?

conf,
namespace,
instance,
version,
external,
serviceDiscovery.fe.attributes)
// Set a watch on the serviceNode
synchronized {
serviceNode = node
}
watchNode()
}

override def deregisterService(): Unit = {
// close the EPHEMERAL_SEQUENTIAL node in zk
if (serviceNode != null) {
// Explicit shutdown and the deletion watcher can deregister concurrently.
val node = synchronized {
val current = serviceNode
serviceNode = null
current
}
if (node != null) {
val path = node.getActualPath
try {
serviceNode.close()
node.close()
} catch {
case e @ (_: IOException | _: KeeperException) =>
error("Failed to close the persistent ephemeral znode" + serviceNode.getActualPath, e)
} finally {
serviceNode = null
error(s"Failed to close the persistent ephemeral znode $path", e)
}
}
}
Expand Down Expand Up @@ -401,11 +407,12 @@ class ZookeeperDiscoveryClient(conf: KyuubiConf) extends DiscoveryClient {
}

private def watchNode(): Unit = {
if (zkClient.checkExists
.usingWatcher(watcher.asInstanceOf[Watcher]).forPath(serviceNode.getActualPath) == null) {
// No node exists, throw exception
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.

if (zkClient.checkExists.usingWatcher(watcher.asInstanceOf[Watcher]).forPath(path) == null) {
throw new KyuubiException(s"Unable to create znode for this Kyuubi " +
s"instance[${watcher.instance}] on ZooKeeper.")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ package org.apache.kyuubi.ha.client.zookeeper
import java.io.{File, IOException}
import java.net.InetAddress
import java.util
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.{CountDownLatch, Executors, TimeUnit}
import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger}
import javax.security.auth.login.Configuration

import scala.collection.JavaConverters._
Expand All @@ -37,8 +38,9 @@ import org.apache.kyuubi.ha.client.zookeeper.ZookeeperClientProvider._
import org.apache.kyuubi.jdbc.hive.strategy.{ServerSelectStrategy, ServerSelectStrategyFactory}
import org.apache.kyuubi.service._
import org.apache.kyuubi.shaded.curator.framework.{CuratorFramework, CuratorFrameworkFactory}
import org.apache.kyuubi.shaded.curator.framework.recipes.nodes.PersistentNode
import org.apache.kyuubi.shaded.curator.retry.ExponentialBackoffRetry
import org.apache.kyuubi.shaded.zookeeper.ZooDefs
import org.apache.kyuubi.shaded.zookeeper.{CreateMode, WatchedEvent, Watcher, ZooDefs}
import org.apache.kyuubi.shaded.zookeeper.data.ACL
import org.apache.kyuubi.util.reflect.ReflectUtils._
import org.apache.kyuubi.zookeeper.EmbeddedZookeeper
Expand Down Expand Up @@ -199,6 +201,104 @@ abstract class ZookeeperDiscoveryClientSuite extends DiscoveryClientTests
}
}

test("node deletion can complete graceful shutdown during explicit deregistration") {
val client = new ZookeeperDiscoveryClient(conf)
val zkClient = getField[CuratorFramework](client, "zkClient")
val path = "/concurrent-deregistration"
val closeStarted = new CountDownLatch(1)
val finishClose = new CountDownLatch(1)
val gracefulShutdown = new CountDownLatch(1)
val closeCalls = new AtomicInteger()
val serviceNode =
new PersistentNode(zkClient, CreateMode.EPHEMERAL, false, path, Array.empty[Byte]) {
override def close(): Unit = {
if (closeCalls.incrementAndGet() == 1) {
closeStarted.countDown()
assert(finishClose.await(10, TimeUnit.SECONDS))
}
}
}
val serviceNodeField = classOf[ZookeeperDiscoveryClient].getDeclaredField("serviceNode")
serviceNodeField.setAccessible(true)
serviceNodeField.set(client, serviceNode)

val discovery = new ServiceDiscovery("test", null) {
override def stopGracefully(isLost: Boolean): Unit = gracefulShutdown.countDown()
}
val watcher = new client.DeRegisterWatcher("test", discovery)
val executor = Executors.newSingleThreadExecutor()
try {
val deregistration = executor.submit(new Runnable {
override def run(): Unit = client.deregisterService()
})
assert(closeStarted.await(10, TimeUnit.SECONDS))
watcher.process(new WatchedEvent(
Watcher.Event.EventType.NodeDeleted,
Watcher.Event.KeeperState.SyncConnected,
path))
assert(gracefulShutdown.getCount == 0)
finishClose.countDown()
deregistration.get(10, TimeUnit.SECONDS)
assert(closeCalls.get() == 1)
} finally {
finishClose.countDown()
executor.shutdownNow()
client.closeClient()
}
}

test("explicit server stop completes while ZooKeeper watch callbacks are queued") {
val namespace = "queued-watch-shutdown"
conf.set(HA_NAMESPACE, namespace).set(KyuubiConf.FRONTEND_THRIFT_BINARY_BIND_PORT, 0)
var discovery: ServiceDiscovery = null
val server = new NoopTBinaryFrontendServer() {
override val frontendServices: Seq[NoopTBinaryFrontendService] = Seq(
new NoopTBinaryFrontendService(this) {
override val discoveryService: Option[Service] = {
discovery = new KyuubiServiceDiscovery(this)
Some(discovery)
}
})
}
server.initialize(conf)
server.start()

val callbackStarted = new CountDownLatch(1)
val releaseCallback = new CountDownLatch(1)
val executor = Executors.newSingleThreadExecutor()
try {
withDiscoveryClient(conf) { observer =>
val basePath = s"/$namespace"
val servicePath = s"$basePath/${observer.getChildren(basePath).head}"
val barrierPath = s"$basePath/barrier"
observer.create(barrierPath, CreateMode.PERSISTENT.name())
val zkClient = getField[CuratorFramework](discovery.discoveryClient, "zkClient")
zkClient.checkExists().usingWatcher(new Watcher {
override def process(event: WatchedEvent): Unit = {
if (event.getType == Watcher.Event.EventType.NodeDataChanged) {
callbackStarted.countDown()
releaseCallback.await()
}
}
}).forPath(barrierPath)
observer.setData(barrierPath, Array[Byte](1))
assert(callbackStarted.await(10, TimeUnit.SECONDS))
observer.setData(servicePath, observer.getData(servicePath))

val shutdown = executor.submit(new Runnable {
override def run(): Unit = server.stop()
})
shutdown.get(10, TimeUnit.SECONDS)
assert(discovery.getServiceState == ServiceState.STOPPED)
assert(observer.pathNonExists(servicePath))
}
} finally {
releaseCallback.countDown()
executor.shutdownNow()
server.stop()
}
}

test("watcher for zookeeper") {
val namespace = "kyuubiwatcher"
var discovery: ServiceDiscovery = null
Expand Down
Loading