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 3d68b6d67e fix: enforce cluster router limits for AdjustPoolSize
(#3364)
3d68b6d67e is described below
commit 3d68b6d67ec48b0c6b2e78f67841eeb39cf6efab
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Sat Jul 18 21:22:09 2026 +0800
fix: enforce cluster router limits for AdjustPoolSize (#3364)
Motivation:
ClusterRouterPool inherited the generic AdjustPoolSize increase path, which
created local routees without applying cluster deployment constraints.
Modification:
Handle positive adjustments with the cluster deployment selector and cap the
number of additions at the requested delta. Add directional tests and
document
the constrained behavior.
Result:
AdjustPoolSize can no longer bypass eligible-node, total-instance, or
per-node-instance limits for cluster router pools.
Tests:
- ClusterRouterSupervisorSpec: passed (6 tests; 5 low-throughput repeats
also passed)
- cluster / Test / test: passed (370 tests)
- docs / paradox: passed
- +headerCheckAll, checkCodeStyle, and +mimaReportBinaryIssues: passed
- validatePullRequest: affected modules passed until docs / Pr-validation /
test;
that task had 634 passed and 1 existing IntegrationDocTest failure
because it
references pekko.dispatch.UnboundedMailbox
- scalafmt --list --mode diff-ref=origin/main and git diff --check: passed
- sortImports: failed in existing tooling with NoSuchMethodError for
scala.meta.package$.Input(); no unrelated output retained
References:
Fixes #3362
---
.../cluster/routing/ClusterRouterConfig.scala | 42 ++++----
.../routing/ClusterRouterSupervisorSpec.scala | 106 +++++++++++++++++++++
docs/src/main/paradox/cluster-routing.md | 4 +
3 files changed, 136 insertions(+), 16 deletions(-)
diff --git
a/cluster/src/main/scala/org/apache/pekko/cluster/routing/ClusterRouterConfig.scala
b/cluster/src/main/scala/org/apache/pekko/cluster/routing/ClusterRouterConfig.scala
index f499c8801a..80131e8125 100644
---
a/cluster/src/main/scala/org/apache/pekko/cluster/routing/ClusterRouterConfig.scala
+++
b/cluster/src/main/scala/org/apache/pekko/cluster/routing/ClusterRouterConfig.scala
@@ -29,6 +29,7 @@ import pekko.japi.Util.immutableSeq
import pekko.remote.RemoteScope
import pekko.routing.ActorRefRoutee
import pekko.routing.ActorSelectionRoutee
+import pekko.routing.AdjustPoolSize
import pekko.routing.Group
import pekko.routing.Pool
import pekko.routing.Resizer
@@ -406,28 +407,37 @@ private[pekko] class ClusterRouterPoolActor(
extends RouterPoolActor(supervisorStrategy)
with ClusterRouterActor {
- override def receive = clusterReceive.orElse(super.receive)
+ override def receive =
+ clusterReceive
+ .orElse({
+ case AdjustPoolSize(change) if change > 0 => addRoutees(change)
+ }: Actor.Receive)
+ .orElse(super.receive)
/**
* Adds routees based on totalInstances and maxInstancesPerNode settings
*/
- override def addRoutees(): Unit = {
- @tailrec
- def doAddRoutees(): Unit = selectDeploymentTarget match {
- case None => // done
- case Some(target) =>
- val routeeProps = cell.routeeProps
- val deploy =
- Deploy(config = ConfigFactory.empty(), routerConfig =
routeeProps.routerConfig, scope = RemoteScope(target))
- val routee = pool.newRoutee(routeeProps.withDeploy(deploy), context)
- // must register each one, since registered routees are used in
selectDeploymentTarget
- cell.addRoutee(routee)
+ override def addRoutees(): Unit = addRoutees(settings.totalInstances)
- // recursion until all created
- doAddRoutees()
- }
+ private def addRoutees(numberOfRoutees: Int): Unit = {
+ @tailrec
+ def doAddRoutees(remaining: Int): Unit =
+ if (remaining > 0)
+ selectDeploymentTarget match {
+ case None => // done
+ case Some(target) =>
+ val routeeProps = cell.routeeProps
+ val deploy =
+ Deploy(config = ConfigFactory.empty(), routerConfig =
routeeProps.routerConfig,
+ scope = RemoteScope(target))
+ val routee = pool.newRoutee(routeeProps.withDeploy(deploy),
context)
+ // must register each one, since registered routees are used in
selectDeploymentTarget
+ cell.addRoutee(routee)
+
+ doAddRoutees(remaining - 1)
+ }
- doAddRoutees()
+ doAddRoutees(numberOfRoutees)
}
def selectDeploymentTarget: Option[Address] = {
diff --git
a/cluster/src/test/scala/org/apache/pekko/cluster/routing/ClusterRouterSupervisorSpec.scala
b/cluster/src/test/scala/org/apache/pekko/cluster/routing/ClusterRouterSupervisorSpec.scala
index 52fcb0933e..c7c0fd4f37 100644
---
a/cluster/src/test/scala/org/apache/pekko/cluster/routing/ClusterRouterSupervisorSpec.scala
+++
b/cluster/src/test/scala/org/apache/pekko/cluster/routing/ClusterRouterSupervisorSpec.scala
@@ -16,7 +16,13 @@ package org.apache.pekko.cluster.routing
import org.apache.pekko
import pekko.actor._
import pekko.actor.OneForOneStrategy
+import pekko.cluster.Cluster
+import pekko.cluster.ClusterEvent.CurrentClusterState
+import pekko.cluster.MemberStatus
+import pekko.routing.AdjustPoolSize
+import pekko.routing.GetRoutees
import pekko.routing.RoundRobinPool
+import pekko.routing.Routees
import pekko.testkit._
object ClusterRouterSupervisorSpec {
@@ -40,6 +46,25 @@ class ClusterRouterSupervisorSpec extends PekkoSpec("""
import ClusterRouterSupervisorSpec._
+ private val cluster = Cluster(system)
+
+ override protected def atStartup(): Unit = {
+ cluster.join(cluster.selfAddress)
+ awaitAssert(cluster.selfMember.status should ===(MemberStatus.Up))
+ }
+
+ private def routeesAfterInitialClusterState(router: ActorRef): Routees = {
+ // Ensure preStart has sent the subscription before asking the cluster
event publisher for a barrier.
+ router.tell(GetRoutees, testActor)
+ expectMsgType[Routees]
+ cluster.sendCurrentClusterState(testActor)
+ expectMsgType[CurrentClusterState]
+
+ // The subscription snapshot was enqueued before the barrier reply, so
this query observes it.
+ router.tell(GetRoutees, testActor)
+ expectMsgType[Routees]
+ }
+
"Cluster aware routers" must {
"use provided supervisor strategy" in {
@@ -59,6 +84,87 @@ class ClusterRouterSupervisorSpec extends PekkoSpec("""
expectMsg("supervised")
}
+ "not create local routees through AdjustPoolSize when local routees are
disabled" in {
+ val router = system.actorOf(
+ ClusterRouterPool(
+ RoundRobinPool(nrOfInstances = 0),
+ ClusterRouterPoolSettings(totalInstances = 1, maxInstancesPerNode =
1, allowLocalRoutees = false))
+ .props(Props(classOf[KillableActor])))
+
+ router ! AdjustPoolSize(1)
+ router.tell(GetRoutees, testActor)
+
+ expectMsg(Routees(Vector.empty))
+ }
+
+ "not create local routees through AdjustPoolSize when local roles do not
match" in {
+ val router = system.actorOf(
+ ClusterRouterPool(
+ RoundRobinPool(nrOfInstances = 0),
+ ClusterRouterPoolSettings(
+ totalInstances = 1,
+ maxInstancesPerNode = 1,
+ allowLocalRoutees = true,
+ useRoles = Set("worker")))
+ .props(Props(classOf[KillableActor])))
+
+ router ! AdjustPoolSize(1)
+ router.tell(GetRoutees, testActor)
+
+ expectMsg(Routees(Vector.empty))
+ }
+
+ "not exceed the per-node limit through AdjustPoolSize" in {
+ val router = system.actorOf(
+ ClusterRouterPool(
+ RoundRobinPool(nrOfInstances = 0),
+ ClusterRouterPoolSettings(totalInstances = 2, maxInstancesPerNode =
1, allowLocalRoutees = true))
+ .props(Props(classOf[KillableActor])))
+
+ router ! AdjustPoolSize(1)
+ router ! AdjustPoolSize(1)
+ router.tell(GetRoutees, testActor)
+
+ expectMsgType[Routees].routees.size should ===(1)
+ }
+
+ "not exceed the total routee limit through AdjustPoolSize" in {
+ val router = system.actorOf(
+ ClusterRouterPool(
+ RoundRobinPool(nrOfInstances = 0),
+ ClusterRouterPoolSettings(totalInstances = 3, maxInstancesPerNode =
3, allowLocalRoutees = true))
+ .props(Props(classOf[KillableActor])))
+
+ routeesAfterInitialClusterState(router).routees.size should ===(3)
+
+ router ! AdjustPoolSize(-2)
+ router.tell(GetRoutees, testActor)
+ expectMsgType[Routees].routees.size should ===(1)
+
+ router ! AdjustPoolSize(5)
+ router.tell(GetRoutees, testActor)
+ expectMsgType[Routees].routees.size should ===(3)
+ }
+
+ "add only the requested number of routees through AdjustPoolSize" in {
+ val router = system.actorOf(
+ ClusterRouterPool(
+ RoundRobinPool(nrOfInstances = 0),
+ ClusterRouterPoolSettings(totalInstances = 3, maxInstancesPerNode =
3, allowLocalRoutees = true))
+ .props(Props(classOf[KillableActor])))
+
+ routeesAfterInitialClusterState(router).routees.size should ===(3)
+
+ router ! AdjustPoolSize(-2)
+ router.tell(GetRoutees, testActor)
+ expectMsgType[Routees].routees.size should ===(1)
+
+ router ! AdjustPoolSize(1)
+ router.tell(GetRoutees, testActor)
+
+ expectMsgType[Routees].routees.size should ===(2)
+ }
+
}
}
diff --git a/docs/src/main/paradox/cluster-routing.md
b/docs/src/main/paradox/cluster-routing.md
index 8d1abce7be..2e0eaba684 100644
--- a/docs/src/main/paradox/cluster-routing.md
+++ b/docs/src/main/paradox/cluster-routing.md
@@ -176,6 +176,10 @@ per node, `max-nr-of-instances-per-node`, will not be
exceeded. By default `max-
is set to a high value (10000) that will result in new routees added to the
router when nodes join the cluster.
Set it to a lower value if you want to limit total number of routees.
+Routees added with @apidoc[routing.AdjustPoolSize] are deployed only to
eligible nodes and are constrained by
+`max-total-nr-of-instances` and `max-nr-of-instances-per-node`. If fewer
eligible slots are available than requested,
+only the available number of routees are added.
+
The same type of router could also have been defined in code:
Scala
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]