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

He-Pin 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 a0e44fd96d feat: define custom lease name for sharding and singleton 
(#3648)
a0e44fd96d is described below

commit a0e44fd96d3c023c0691d1505cd6898acc30b0c7
Author: PJ Fanning <[email protected]>
AuthorDate: Fri Oct 23 04:38:43 2026 +0100

    feat: define custom lease name for sharding and singleton (#3648)
    
    Motivation:
    
    The lease names used by Cluster Sharding shards and Cluster Singletons are 
derived from the
    ActorSystem name and the entity type name or singleton actor path. Those 
names can become too
    long for some lease implementations, such as the Kubernetes lease, and 
there was no way to
    choose a shorter name.
    
    Modification:
    
    - Add a `leaseName` to `LeaseUsageSettings` (empty means the derived name, 
as before), with
      `withLeaseName` and the Java `getLeaseName`. The existing two-argument 
constructor is kept and
      is now public.
    - `Shard` uses `<leaseName>-<shardId>` when a custom lease name is defined.
    - `ClusterSingletonManager` uses the custom lease name as is when defined.
    - The lease name is intentionally not read from config, since every 
singleton and entity type
      must have a unique lease name; reference.conf documents how to define it 
programmatically.
    - Document the uniqueness requirement on the `withLeaseSettings` methods of 
the classic and
      typed sharding and singleton settings.
    - Tests in `ShardWithLeaseSpec` and `ClusterSingletonLeaseSpec`.
    
    Result:
    
    Users can define shorter custom lease names for sharding and singletons. 
The default lease
    names are unchanged.
    
    Tests:
    
    Not run locally - left to CI
    
    References:
    Ported from (Apache-2.0 since 2026-10-23):
    - https://github.com/akka/akka-core/pull/32122
    
    Notes for reviewers:
    - Binary compatibility: only additions (new primary constructor, 
`leaseName`, `getLeaseName`,
      `withLeaseName`); the existing two-argument constructor is retained. No 
MiMa excludes added.
    - Wire compatibility: no serialized messages or protobuf changed.
    - Config defaults: no new config keys; reference.conf only gains comments. 
Default lease names
      are unchanged.
    - Rolling upgrade: with no custom name, old and new nodes use the same 
lease names. If a custom
      lease name is introduced, all nodes must use it, so introducing one 
during a rolling upgrade
      means old and new nodes use different leases; do it with a full cluster 
restart.
---
 .../sharding/typed/ClusterShardingSettings.scala   |  5 ++++
 cluster-sharding/src/main/resources/reference.conf |  7 +++++
 .../cluster/sharding/ClusterShardingSettings.scala |  5 ++++
 .../org/apache/pekko/cluster/sharding/Shard.scala  | 12 ++++----
 .../cluster/sharding/ShardWithLeaseSpec.scala      | 23 ++++++++++++--
 cluster-tools/src/main/resources/reference.conf    |  6 ++++
 .../singleton/ClusterSingletonManager.scala        | 16 ++++++----
 .../singleton/ClusterSingletonLeaseSpec.scala      | 21 +++++++++++++
 .../pekko/cluster/typed/ClusterSingleton.scala     | 10 +++++++
 .../coordination/lease/LeaseUsageSettings.scala    | 35 ++++++++++++++++++++--
 10 files changed, 125 insertions(+), 15 deletions(-)

diff --git 
a/cluster-sharding-typed/src/main/scala/org/apache/pekko/cluster/sharding/typed/ClusterShardingSettings.scala
 
b/cluster-sharding-typed/src/main/scala/org/apache/pekko/cluster/sharding/typed/ClusterShardingSettings.scala
index 2b3933b66f..6e5c554bec 100644
--- 
a/cluster-sharding-typed/src/main/scala/org/apache/pekko/cluster/sharding/typed/ClusterShardingSettings.scala
+++ 
b/cluster-sharding-typed/src/main/scala/org/apache/pekko/cluster/sharding/typed/ClusterShardingSettings.scala
@@ -846,6 +846,11 @@ final class ClusterShardingSettings(
   def withShardRegionQueryTimeout(duration: java.time.Duration): 
ClusterShardingSettings =
     copy(shardRegionQueryTimeout = duration.toScala)
 
+  /**
+   * Note that if you define a custom lease name and have several sharding 
entity types each one must have a unique
+   * lease name. If the lease name is undefined it will be derived from 
ActorSystem name and shard name,
+   * but that may result in too long lease names.
+   */
   def withLeaseSettings(leaseSettings: LeaseUsageSettings) = 
copy(leaseSettings = Option(leaseSettings))
 
   /**
diff --git a/cluster-sharding/src/main/resources/reference.conf 
b/cluster-sharding/src/main/resources/reference.conf
index 1127d6962c..61bce2fe58 100644
--- a/cluster-sharding/src/main/resources/reference.conf
+++ b/cluster-sharding/src/main/resources/reference.conf
@@ -428,6 +428,13 @@ pekko.cluster.sharding {
   # The interval between retries for acquiring the lease
   lease-retry-interval = 5s
 
+  # A custom lease name can be defined with `LeaseUsageSettings.withLeaseName` 
in the
+  # leaseSettings of ClusterShardingSettings. The shard identifier is appended 
to it.
+  # It is intentionally not defined in config, because if you have several 
entity types
+  # each one must have a unique lease name. If undefined, the lease name is 
derived from
+  # the ActorSystem name, entity type name and shard identifier, but that may 
result in
+  # too long lease names.
+
   # Provide a higher level of details in the debug logs, often per routed 
message. Be careful about enabling
   # in production systems.
   verbose-debug-logging = off
diff --git 
a/cluster-sharding/src/main/scala/org/apache/pekko/cluster/sharding/ClusterShardingSettings.scala
 
b/cluster-sharding/src/main/scala/org/apache/pekko/cluster/sharding/ClusterShardingSettings.scala
index 64b7c0ab6e..2559fc1be1 100644
--- 
a/cluster-sharding/src/main/scala/org/apache/pekko/cluster/sharding/ClusterShardingSettings.scala
+++ 
b/cluster-sharding/src/main/scala/org/apache/pekko/cluster/sharding/ClusterShardingSettings.scala
@@ -974,6 +974,11 @@ final class ClusterShardingSettings(
   def withShardRegionQueryTimeout(duration: java.time.Duration): 
ClusterShardingSettings =
     copy(shardRegionQueryTimeout = duration.toScala)
 
+  /**
+   * Note that if you define a custom lease name and have several sharding 
entity types each one must have a unique
+   * lease name. If the lease name is undefined it will be derived from 
ActorSystem name and shard name,
+   * but that may result in too long lease names.
+   */
   def withLeaseSettings(leaseSettings: LeaseUsageSettings): 
ClusterShardingSettings =
     copy(leaseSettings = Some(leaseSettings))
 
diff --git 
a/cluster-sharding/src/main/scala/org/apache/pekko/cluster/sharding/Shard.scala 
b/cluster-sharding/src/main/scala/org/apache/pekko/cluster/sharding/Shard.scala
index 57785da53b..560bed9d46 100644
--- 
a/cluster-sharding/src/main/scala/org/apache/pekko/cluster/sharding/Shard.scala
+++ 
b/cluster-sharding/src/main/scala/org/apache/pekko/cluster/sharding/Shard.scala
@@ -493,11 +493,13 @@ private[pekko] class Shard(
     context.system.scheduler.scheduleWithFixedDelay(interval, interval, self, 
PassivateIntervalTick)
   }
 
-  private val lease = settings.leaseSettings.map(ls =>
-    LeaseProvider(context.system).getLease(
-      s"${context.system.name}-shard-$typeName-$shardId",
-      ls.leaseImplementation,
-      Cluster(context.system).selfAddress.hostPort))
+  private val lease = settings.leaseSettings.map { ls =>
+    val leaseName =
+      if (ls.leaseName.isEmpty) 
s"${context.system.name}-shard-$typeName-$shardId"
+      else s"${ls.leaseName}-$shardId"
+    LeaseProvider(context.system).getLease(leaseName, ls.leaseImplementation,
+      Cluster(context.system).selfAddress.hostPort)
+  }
 
   private val leaseRetryInterval = settings.leaseSettings match {
     case Some(l) => l.leaseRetryInterval
diff --git 
a/cluster-sharding/src/test/scala/org/apache/pekko/cluster/sharding/ShardWithLeaseSpec.scala
 
b/cluster-sharding/src/test/scala/org/apache/pekko/cluster/sharding/ShardWithLeaseSpec.scala
index 800d1ffcf9..302dabd54a 100644
--- 
a/cluster-sharding/src/test/scala/org/apache/pekko/cluster/sharding/ShardWithLeaseSpec.scala
+++ 
b/cluster-sharding/src/test/scala/org/apache/pekko/cluster/sharding/ShardWithLeaseSpec.scala
@@ -98,6 +98,20 @@ class ShardWithLeaseSpec extends 
PekkoSpec(ShardWithLeaseSpec.config) with WithL
       probe.expectMsg("ack hello")
     }
 
+    "use custom lease name with shard id appended" in new Setup {
+      override def customLeaseName: String = "custom-shard-lease"
+      settings.leaseSettings.get.leaseName shouldBe customLeaseName
+      val probe = TestProbe()
+      sharding.tell(EntityEnvelope(1, "hello"), probe.ref)
+      probe.expectNoMessage(shortDuration)
+      leaseFor("1").initialPromise.complete(Success(true))
+      testLeaseExt.getTestLease("custom-shard-lease-1") // lease name with 
shard id appended
+      probe.expectMsg("ack hello")
+      intercept[IllegalStateException] {
+        testLeaseExt.getTestLease(s"${system.name}-shard-$typeName-1")
+      }
+    }
+
     "retry if lease acquire returns false" in new Setup {
       val probe = TestProbe()
       val lease =
@@ -153,7 +167,10 @@ class ShardWithLeaseSpec extends 
PekkoSpec(ShardWithLeaseSpec.config) with WithL
   var typeIdx = 0
 
   trait Setup {
-    val settings = ClusterShardingSettings(system).withLeaseSettings(new 
LeaseUsageSettings("test-lease", 2.seconds))
+    def customLeaseName: String = ""
+
+    val settings = ClusterShardingSettings(system).withLeaseSettings(
+      new LeaseUsageSettings("test-lease", 
2.seconds).withLeaseName(customLeaseName))
 
     // unique type name for each test
     val typeName = {
@@ -165,7 +182,9 @@ class ShardWithLeaseSpec extends 
PekkoSpec(ShardWithLeaseSpec.config) with WithL
       ClusterSharding(system).start(typeName, Props(new EntityActor()), 
settings, extractEntityId, extractShardId)
 
     def leaseFor(shardId: ShardId) = awaitAssert {
-      val leaseName = s"${system.name}-shard-$typeName-$shardId"
+      val leaseName =
+        if (customLeaseName.isEmpty) s"${system.name}-shard-$typeName-$shardId"
+        else s"$customLeaseName-$shardId"
       testLeaseExt.getTestLease(leaseName)
     }
   }
diff --git a/cluster-tools/src/main/resources/reference.conf 
b/cluster-tools/src/main/resources/reference.conf
index cc18387346..e43467bb48 100644
--- a/cluster-tools/src/main/resources/reference.conf
+++ b/cluster-tools/src/main/resources/reference.conf
@@ -191,6 +191,12 @@ pekko.cluster.singleton {
 
   # The interval between retries for acquiring the lease
   lease-retry-interval = 5s
+
+  # A custom lease name can be defined with `LeaseUsageSettings.withLeaseName` 
in the
+  # leaseSettings of ClusterSingletonManagerSettings. It is intentionally not 
defined in
+  # config, because if you have several singletons each one must have a unique 
lease name.
+  # If undefined, the lease name is derived from the ActorSystem name and the 
singleton actor
+  # path, but that may result in too long lease names.
 }
 # //#singleton-config
 
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 6c720e2b11..f7465ac7c1 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
@@ -153,6 +153,11 @@ final class ClusterSingletonManagerSettings(
   def withHandOverRetryInterval(retryInterval: FiniteDuration): 
ClusterSingletonManagerSettings =
     copy(handOverRetryInterval = retryInterval)
 
+  /**
+   * Note that if you define a custom lease name and have several singletons 
each one must have a unique
+   * lease name. If the lease name is undefined it will be derived from 
ActorSystem name and singleton
+   * actor path, but that may result in too long lease names.
+   */
   def withLeaseSettings(leaseSettings: LeaseUsageSettings): 
ClusterSingletonManagerSettings =
     copy(leaseSettings = Some(leaseSettings))
 
@@ -511,13 +516,14 @@ class ClusterSingletonManager(singletonProps: Props, 
terminationMessage: Any, se
     role.forall(cluster.selfRoles.contains),
     s"This cluster member [${cluster.selfAddress}] doesn't have the role 
[$role]")
 
-  private val singletonLeaseName = 
s"${context.system.name}-singleton-${self.path}"
-
   override val log: MarkerLoggingAdapter = Logging.withMarker(context.system, 
this)
 
-  val lease: Option[Lease] = settings.leaseSettings.map(settings =>
-    LeaseProvider(context.system)
-      .getLease(singletonLeaseName, settings.leaseImplementation, 
cluster.selfAddress.hostPort))
+  val lease: Option[Lease] = settings.leaseSettings.map { settings =>
+    val leaseName =
+      if (settings.leaseName.isEmpty) 
s"${context.system.name}-singleton-${self.path}"
+      else settings.leaseName
+    LeaseProvider(context.system).getLease(leaseName, 
settings.leaseImplementation, cluster.selfAddress.hostPort)
+  }
   val leaseRetryInterval: FiniteDuration = settings.leaseSettings match {
     case Some(s) => s.leaseRetryInterval
     case None    => 5.seconds // won't be used
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 c0eafdfa64..aefac3e829 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
@@ -196,6 +196,27 @@ class ClusterSingletonLeaseSpec extends 
PekkoSpec(ConfigFactory.parseString("""
       lifecycleProbe.expectMsg("preStart")
     }
 
+    "use custom lease name" in {
+      val probe = TestProbe()
+      val defaultSettings = nextSettings()
+      val customLeaseName = s"custom-lease-${defaultSettings.singletonName}"
+      val settings =
+        
defaultSettings.withLeaseSettings(defaultSettings.leaseSettings.get.withLeaseName(customLeaseName))
+      settings.leaseSettings.get.leaseName shouldBe customLeaseName
+      system.actorOf(
+        ClusterSingletonManager.props(Props(new 
ImportantSingleton(probe.ref)), PoisonPill, settings),
+        settings.singletonName)
+      val testLease = awaitAssert {
+        testLeaseExt.getTestLease(customLeaseName)
+      } // allow singleton manager to create the lease
+      probe.expectNoMessage(shortDuration)
+      testLease.initialPromise.complete(Success(true))
+      probe.expectMsg("preStart")
+      intercept[IllegalStateException] {
+        testLeaseExt.getTestLease(leaseNameFor(settings))
+      }
+    }
+
     "release lease when leaving oldest" in {
       val singletonProbe = TestProbe()
       val settings = nextSettings()
diff --git 
a/cluster-typed/src/main/scala/org/apache/pekko/cluster/typed/ClusterSingleton.scala
 
b/cluster-typed/src/main/scala/org/apache/pekko/cluster/typed/ClusterSingleton.scala
index 1a3b122a6c..d1858b0e1b 100644
--- 
a/cluster-typed/src/main/scala/org/apache/pekko/cluster/typed/ClusterSingleton.scala
+++ 
b/cluster-typed/src/main/scala/org/apache/pekko/cluster/typed/ClusterSingleton.scala
@@ -84,6 +84,11 @@ final class ClusterSingletonSettings(
 
   def withBufferSize(bufferSize: Int): ClusterSingletonSettings = 
copy(bufferSize = bufferSize)
 
+  /**
+   * Note that if you define a custom lease name and have several singletons 
each one must have a unique
+   * lease name. If the lease name is undefined it will be derived from 
ActorSystem name and singleton
+   * actor path, but that may result in too long lease names.
+   */
   def withLeaseSettings(leaseSettings: LeaseUsageSettings) = 
copy(leaseSettings = Option(leaseSettings))
 
   private def copy(
@@ -312,6 +317,11 @@ final class ClusterSingletonManagerSettings(
   def withHandOverRetryInterval(retryInterval: java.time.Duration): 
ClusterSingletonManagerSettings =
     withHandOverRetryInterval(retryInterval.toScala)
 
+  /**
+   * Note that if you define a custom lease name and have several singletons 
each one must have a unique
+   * lease name. If the lease name is undefined it will be derived from 
ActorSystem name and singleton
+   * actor path, but that may result in too long lease names.
+   */
   def withLeaseSettings(leaseSettings: LeaseUsageSettings) = 
copy(leaseSettings = Option(leaseSettings))
 
   private def copy(
diff --git 
a/coordination/src/main/scala/org/apache/pekko/coordination/lease/LeaseUsageSettings.scala
 
b/coordination/src/main/scala/org/apache/pekko/coordination/lease/LeaseUsageSettings.scala
index 8fbae9a8eb..ef7bb130d2 100644
--- 
a/coordination/src/main/scala/org/apache/pekko/coordination/lease/LeaseUsageSettings.scala
+++ 
b/coordination/src/main/scala/org/apache/pekko/coordination/lease/LeaseUsageSettings.scala
@@ -19,9 +19,38 @@ import scala.jdk.DurationConverters._
 import org.apache.pekko
 import pekko.util.PrettyDuration._
 
-final class LeaseUsageSettings private[pekko] (val leaseImplementation: String,
-    val leaseRetryInterval: FiniteDuration) {
+/**
+ * Note that if you define a custom lease name and have several Cluster 
Singletons or Cluster Sharding
+ * entity types each one must have a unique lease name. If the lease name is 
undefined it will be derived
+ * from ActorSystem name and other component names, but that may result in too 
long lease names.
+ */
+final class LeaseUsageSettings private[pekko] (
+    val leaseImplementation: String,
+    val leaseRetryInterval: FiniteDuration,
+    val leaseName: String) {
+
+  def this(leaseImplementation: String, leaseRetryInterval: FiniteDuration) =
+    this(leaseImplementation, leaseRetryInterval, leaseName = "")
+
   def getLeaseRetryInterval(): java.time.Duration = leaseRetryInterval.toJava
 
-  override def toString = s"LeaseUsageSettings($leaseImplementation, 
${leaseRetryInterval.pretty})"
+  /**
+   * Java API: The custom lease name, empty if the lease name is derived from 
the
+   * ActorSystem name and other component names.
+   */
+  def getLeaseName(): String = leaseName
+
+  /**
+   * Define a custom lease name. Note that if you have several Cluster 
Singletons or Cluster Sharding
+   * entity types each one must have a unique lease name. For Cluster Sharding 
the shard identifier
+   * is appended to the custom lease name, so that each shard has its own 
lease.
+   * If the lease name is undefined (empty) it will be derived from 
ActorSystem name and other
+   * component names, but that may result in too long lease names.
+   */
+  def withLeaseName(name: String): LeaseUsageSettings =
+    new LeaseUsageSettings(leaseImplementation, leaseRetryInterval, name)
+
+  override def toString =
+    if (leaseName.isEmpty) s"LeaseUsageSettings($leaseImplementation, 
${leaseRetryInterval.pretty})"
+    else s"LeaseUsageSettings($leaseImplementation, 
${leaseRetryInterval.pretty}, $leaseName)"
 }


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

Reply via email to