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]

Reply via email to