This is an automated email from the ASF dual-hosted git repository. He-Pin pushed a commit to branch fix-3685-singleton-release-failure in repository https://gitbox.apache.org/repos/asf/pekko.git
commit 23a08cede4534a885602804f06711fe020410fc3 Author: 虎鸣 <[email protected]> AuthorDate: Sat Oct 24 23:16:30 2026 +0800 fix: handle failed lease release on the Oldest transition in ClusterSingletonManager Motivation: The Oldest -> _ transition pipes the release future without a recover, so a failed release arrives as an unhandled Status.Failure instead of reaching the ReleaseLeaseFailure handler. The failure to release the lease is then never logged, exactly when the diagnostic matters most. Modification: Mirror the AcquiringLease path and convert a failed release future into ReleaseLeaseFailure. Add TestLease.setNextReleaseResult and a spec case asserting the failed release is logged by the ReleaseLeaseFailure handler. Result: A failed lease release on the oldest transition is handled and logged like the AcquiringLease path instead of escaping as an unhandled message. Tests: - sbt "cluster-tools / Test / testOnly org.apache.pekko.cluster.singleton.ClusterSingletonLeaseSpec": 9/9 passed - Reverted the fix and re-ran the new test only: failed as expected (directional) References: Fixes #3685 --- .../singleton/ClusterSingletonManager.scala | 4 +++- .../singleton/ClusterSingletonLeaseSpec.scala | 24 ++++++++++++++++++++++ .../pekko/coordination/lease/TestLease.scala | 6 +++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/cluster-tools/src/main/scala/org/apache/pekko/cluster/singleton/ClusterSingletonManager.scala b/cluster-tools/src/main/scala/org/apache/pekko/cluster/singleton/ClusterSingletonManager.scala index 4bec4cc4fc..915562bf1c 100644 --- a/cluster-tools/src/main/scala/org/apache/pekko/cluster/singleton/ClusterSingletonManager.scala +++ b/cluster-tools/src/main/scala/org/apache/pekko/cluster/singleton/ClusterSingletonManager.scala @@ -1236,7 +1236,9 @@ class ClusterSingletonManager(singletonProps: Props, terminationMessage: Any, se lease.foreach { l => logInfo("Releasing lease as leaving Oldest") import context.dispatcher - pipe(l.release().map(ReleaseLeaseResult(_))).to(self) + pipe(l.release().map[Any](ReleaseLeaseResult(_)).recover { + case t => ReleaseLeaseFailure(t) + }).to(self) } } diff --git a/cluster-tools/src/test/scala/org/apache/pekko/cluster/singleton/ClusterSingletonLeaseSpec.scala b/cluster-tools/src/test/scala/org/apache/pekko/cluster/singleton/ClusterSingletonLeaseSpec.scala index 78525df297..9124355aba 100644 --- a/cluster-tools/src/test/scala/org/apache/pekko/cluster/singleton/ClusterSingletonLeaseSpec.scala +++ b/cluster-tools/src/test/scala/org/apache/pekko/cluster/singleton/ClusterSingletonLeaseSpec.scala @@ -15,6 +15,7 @@ package org.apache.pekko.cluster.singleton import java.util.concurrent.atomic.AtomicInteger +import scala.concurrent.Future import scala.concurrent.Promise import scala.concurrent.duration._ import scala.util.Success @@ -30,6 +31,7 @@ import pekko.cluster.Cluster import pekko.cluster.MemberStatus import pekko.coordination.lease.TestLease import pekko.coordination.lease.TestLeaseExt +import pekko.testkit.EventFilter import pekko.testkit.PekkoSpec import pekko.testkit.TestException import pekko.testkit.TestProbe @@ -217,6 +219,28 @@ class ClusterSingletonLeaseSpec extends PekkoSpec(ConfigFactory.parseString(""" } } + "log error when lease release fails leaving oldest" in { + val lifecycleProbe = TestProbe() + val settings = nextSettings() + system.actorOf( + ClusterSingletonManager.props(Props(new ImportantSingleton(lifecycleProbe.ref)), PoisonPill, settings), + settings.singletonName) + val testLease = awaitAssert { + testLeaseExt.getTestLease(leaseNameFor(settings)) + } // allow singleton manager to create the lease + testLease.probe.expectMsg(AcquireReq(leaseOwner)) + testLease.initialPromise.complete(Success(true)) + lifecycleProbe.expectMsg("preStart") + testLease.setNextReleaseResult(Future.failed(TestException("no release for you"))) + // lease loss makes the manager stop the singleton and transition out of Oldest, + // which releases the lease; the failed release must be logged rather than + // escaping as an unhandled Status.Failure + EventFilter[TestException](pattern = ".*Failed to release lease.*", occurrences = 1).intercept { + testLease.getCurrentCallback()(Some(TestException("lease lost"))) + testLease.probe.expectMsg(ReleaseReq(leaseOwner)) + } + } + "release lease when leaving oldest" in { val singletonProbe = TestProbe() val settings = nextSettings() diff --git a/coordination/src/test/scala/org/apache/pekko/coordination/lease/TestLease.scala b/coordination/src/test/scala/org/apache/pekko/coordination/lease/TestLease.scala index 71b218b696..75091b9b1a 100644 --- a/coordination/src/test/scala/org/apache/pekko/coordination/lease/TestLease.scala +++ b/coordination/src/test/scala/org/apache/pekko/coordination/lease/TestLease.scala @@ -77,6 +77,7 @@ class TestLease(settings: LeaseSettings, system: ExtendedActorSystem) extends Le val initialPromise = Promise[Boolean]() private val nextAcquireResult = new AtomicReference[Future[Boolean]](initialPromise.future) + private val nextReleaseResult = new AtomicReference[Future[Boolean]](Future.successful(true)) private val nextCheckLeaseResult = new AtomicReference[Boolean](false) private val currentCallBack = new AtomicReference[Option[Throwable] => Unit](_ => ()) @@ -87,6 +88,9 @@ class TestLease(settings: LeaseSettings, system: ExtendedActorSystem) extends Le def setNextAcquireResult(next: Future[Boolean]): Unit = nextAcquireResult.set(next) + def setNextReleaseResult(next: Future[Boolean]): Unit = + nextReleaseResult.set(next) + def setNextCheckLeaseResult(value: Boolean): Unit = nextCheckLeaseResult.set(value) @@ -100,7 +104,7 @@ class TestLease(settings: LeaseSettings, system: ExtendedActorSystem) extends Le override def release(): Future[Boolean] = { probe.ref ! ReleaseReq(settings.ownerName) - Future.successful(true) + nextReleaseResult.get() } override def checkLease(): Boolean = nextCheckLeaseResult.get --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
