This is an automated email from the ASF dual-hosted git repository.

Philippus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git


The following commit(s) were added to refs/heads/main by this push:
     new 54c60e2061 Add LeaseHealthCheck (#3423)
54c60e2061 is described below

commit 54c60e20611ad63550ae4a6e0b76196d58c32844
Author: Philippus Baalman <[email protected]>
AuthorDate: Fri Aug 14 21:09:16 2026 +0200

    Add LeaseHealthCheck (#3423)
    
    * Add LeaseHealthCheck
    
    * Optimize imports
    
    * Make val private
    
    * Share same uuid between lease and owner name
    
    * Scope state to each test
    
    * Update LeaseHealthCheck ScalaDoc comments
    
    Add documentation for LeaseHealthCheck class.
    
    ---------
    
    Co-authored-by: PJ Fanning <[email protected]>
---
 .../coordination/lease/LeaseHealthCheck.scala      |  90 +++++++++++++++
 .../coordination/lease/LeaseHealthCheckSpec.scala  | 128 +++++++++++++++++++++
 2 files changed, 218 insertions(+)

diff --git 
a/coordination/src/main/scala/org/apache/pekko/coordination/lease/LeaseHealthCheck.scala
 
b/coordination/src/main/scala/org/apache/pekko/coordination/lease/LeaseHealthCheck.scala
new file mode 100644
index 0000000000..2eda0c245e
--- /dev/null
+++ 
b/coordination/src/main/scala/org/apache/pekko/coordination/lease/LeaseHealthCheck.scala
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pekko.coordination.lease
+
+import java.util.UUID
+
+import scala.concurrent.{ ExecutionContext, Future }
+import scala.util.{ Failure, Success }
+import scala.annotation.nowarn
+
+import org.apache.pekko
+import pekko.actor.ActorSystem
+import pekko.coordination.lease.scaladsl.{ Lease, LeaseProvider }
+import pekko.event.Logging
+import pekko.pattern.AskTimeoutException
+
+/**
+ * Performs a lease health check by attempting to acquire and immediately 
release a test lease.
+ *
+ * Once the first check succeeds, subsequent checks return true without 
contacting the API.
+ * This is a quick connectivity test to verify the lease API is accessible.
+ *
+ * Returns true if:
+ * - Lease is successfully acquired (true)
+ * - Lease cannot be acquired due to conflict (another owner has it, but the 
API is reachable) (false)
+ *
+ * Returns false if:
+ * - Any exception occurs (LeaseException, AskTimeoutException, etc.)
+ *
+ * @since 2.0.0
+ */
+class LeaseHealthCheck(system: ActorSystem, leaseProviderName: String) extends 
(() => Future[Boolean]) {
+
+  private implicit val executionContext: ExecutionContext = system.dispatcher
+
+  private val log = Logging(system, classOf[LeaseHealthCheck])
+
+  @volatile private var healthCheckPassed = false
+
+  private val randomUUIDString = UUID.randomUUID().toString
+  private val leaseName = s"lease-$randomUUIDString"
+  private val ownerName = s"owner-$randomUUIDString"
+
+  protected val lease: Lease = LeaseProvider(system).getLease(leaseName, 
leaseProviderName, ownerName)
+
+  override def apply(): Future[Boolean] = check()
+
+  @nowarn("msg=match may not be exhaustive")
+  def check(): Future[Boolean] = {
+    if (healthCheckPassed) {
+      Future.successful(true)
+    } else {
+      lease.acquire().transform {
+        case Success(true) =>
+          healthCheckPassed = true
+          log.info(s"lease $leaseName from $ownerName returned true")
+          lease.release()
+          Success(true)
+        case Success(false) =>
+          log.info(s"lease $leaseName from $ownerName returned false")
+          healthCheckPassed = true
+          Success(true)
+        case Failure(e: LeaseException) =>
+          log.warning(s"lease $leaseName from $ownerName returned a 
LeaseException ${e.getMessage}")
+          Success(false)
+        case Failure(e: AskTimeoutException) =>
+          log.warning(s"lease $leaseName from $ownerName returned an 
AskTimeoutException ${e.getMessage}")
+          Success(false)
+        case Failure(e: Exception) =>
+          log.warning(s"lease $leaseName from $ownerName returned an Exception 
${e.getMessage}")
+          Success(false)
+      }
+    }
+  }
+}
diff --git 
a/coordination/src/test/scala/org/apache/pekko/coordination/lease/LeaseHealthCheckSpec.scala
 
b/coordination/src/test/scala/org/apache/pekko/coordination/lease/LeaseHealthCheckSpec.scala
new file mode 100644
index 0000000000..0a99f9d60e
--- /dev/null
+++ 
b/coordination/src/test/scala/org/apache/pekko/coordination/lease/LeaseHealthCheckSpec.scala
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pekko.coordination.lease
+
+import scala.concurrent.Future
+import scala.concurrent.duration._
+
+import org.apache.pekko
+import pekko.coordination.lease.scaladsl.Lease
+import pekko.pattern.AskTimeoutException
+import pekko.testkit.PekkoSpec
+
+import com.typesafe.config.{ Config, ConfigFactory }
+import org.scalatest.concurrent.ScalaFutures
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpecLike
+
+class LeaseHealthCheckSpec
+    extends PekkoSpec(LeaseHealthCheckSpec.config)
+    with ScalaFutures
+    with AnyWordSpecLike
+    with Matchers {
+
+  import LeaseHealthCheckSpec._
+
+  override implicit val patience: PatienceConfig = PatienceConfig(5.seconds)
+
+  private def newTestLease(): MockLease with MockLeaseState =
+    new MockLease(mockLeaseSettings) with MockLeaseState
+
+  private def healthCheckWith(testLease: Lease): LeaseHealthCheck =
+    new LeaseHealthCheck(system, "mock-lease") {
+      override protected val lease: Lease = testLease
+    }
+
+  "LeaseHealthCheckSpec" should {
+    "return true and release lease on successful acquisition" in {
+      val lease = newTestLease()
+      lease.nextAcquire = () => Future.successful(true)
+
+      healthCheckWith(lease).check().futureValue shouldEqual true
+      lease.releaseCalled shouldEqual true
+    }
+
+    "not call acquire again after healthCheckPassed is true" in {
+      val lease = newTestLease()
+      lease.nextAcquire = () => Future.successful(true)
+
+      val healthcheck = healthCheckWith(lease)
+      healthcheck.check().futureValue shouldEqual true
+      lease.acquireCalls shouldEqual 1
+
+      healthcheck.check().futureValue shouldEqual true
+      lease.acquireCalls shouldEqual 1
+    }
+
+    "return true on lease conflict" in {
+      val lease = newTestLease()
+      lease.nextAcquire = () => Future.successful(false)
+      healthCheckWith(lease).check().futureValue shouldEqual true
+    }
+
+    "return false on LeaseException" in {
+      val lease = newTestLease()
+      lease.nextAcquire = () => Future.failed(new LeaseException("API error"))
+      healthCheckWith(lease).check().futureValue shouldEqual false
+    }
+
+    "return false on AskTimeoutException" in {
+      val lease = newTestLease()
+      lease.nextAcquire = () => Future.failed(new 
AskTimeoutException("timeout"))
+      healthCheckWith(lease).check().futureValue shouldEqual false
+    }
+
+    "return false on generic Exception" in {
+      val lease = newTestLease()
+      lease.nextAcquire = () => Future.failed(new RuntimeException("generic 
error"))
+      healthCheckWith(lease).check().futureValue shouldEqual false
+    }
+  }
+}
+
+object LeaseHealthCheckSpec {
+  val config: Config = ConfigFactory.parseString(s"""
+  mock-lease {
+    lease-class = "${classOf[LeaseHealthCheckSpec.MockLease].getName}"
+    heartbeat-timeout = 100s
+    heartbeat-interval = 1s
+    lease-operation-timeout = 2s
+  }
+  """)
+
+  private val mockLeaseSettings = 
LeaseSettings(config.getConfig("mock-lease"), "test-lease", "test-owner")
+
+  trait MockLeaseState {
+    @volatile var nextAcquire: () => Future[Boolean] = () => 
Future.successful(true)
+    @volatile var releaseCalled: Boolean = false
+    @volatile var acquireCalls: Int = 0
+  }
+
+  class MockLease(settings: LeaseSettings) extends Lease(settings) with 
MockLeaseState {
+    override def acquire(): Future[Boolean] = {
+      acquireCalls += 1
+      nextAcquire()
+    }
+    override def acquire(callback: Option[Throwable] => Unit): Future[Boolean] 
= acquire()
+    override def release(): Future[Boolean] = {
+      releaseCalled = true
+      Future.successful(true)
+    }
+    override def checkLease(): Boolean = true
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to