This is an automated email from the ASF dual-hosted git repository.
zaynt4606 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/celeborn.git
The following commit(s) were added to refs/heads/main by this push:
new 6b6efe2c2e [CELEBORN-2398] Allow ChangePartitionManager to refresh
candidate workers without allocating slots
6b6efe2c2e is described below
commit 6b6efe2c2e7a0efef26b12419c7f2f50152767f0
Author: Kalvin2077 <[email protected]>
AuthorDate: Tue Sep 15 10:46:52 2026 +0800
[CELEBORN-2398] Allow ChangePartitionManager to refresh candidate workers
without allocating slots
### What changes were proposed in this pull request?
This PR introduces a Protobuf-based
`RequestWorkers`/`RequestWorkersResponse` RPC for read-only worker discovery.
On the Master side, the new handler selects currently available workers
after applying client exclusions and tag filters. It limits the response using
the smaller of `celeborn.client.slot.assign.maxWorkers` and
`celeborn.master.splitSlot.assign.maxWorkers`, while preserving the minimum
worker count required for replication. When authentication is enabled,
application metadata is pushed to the selected workers.
On the client side, LifecycleManager periodically requests workers from the
Master, creates endpoints for newly selected workers, records connection
failures, and maintains an endpoint-ready worker pool. ChangePartitionManager
uses that pool for change-partition requests and falls back to the shuffle's
existing worker snapshots when no refreshed candidates are available.
The PR also makes the following configuration changes:
- removes `celeborn.client.shuffle.dynamicResourceFactor`;
- adds `celeborn.client.shuffle.dynamicResource.updateTime`, defaulting to
`30s`;
- adds `celeborn.master.splitSlot.assign.maxWorkers`, defaulting to `500`;
- documents the breaking client configuration change in the migration guide.
### Why are the changes needed?
The existing factor-based logic refreshes candidates only after enough
workers from the shuffle's original allocation become unavailable. It therefore
cannot use newly added workers during normal workers scale-out while the
original workers remain healthy.
Requesting slots merely to discover workers also mixes worker discovery
with slot allocation and mutates Master shuffle state. A dedicated read-only
RPC allows ChangePartitionManager to use the current cluster membership without
those side effects, while rate limiting and worker-count caps prevent excessive
RPC connections and oversized responses.
### Does this PR resolve a correctness bug?
- [ ] Yes
### Does this PR introduce _any_ user-facing change?
- [x] Yes
Users of `celeborn.client.shuffle.dynamicResourceFactor` must migrate to
`celeborn.client.shuffle.dynamicResource.updateTime`.
### How was this patch tested?
- Added unit tests for Worker selection, storage eligibility, dynamic
Worker merging, deduplication, and excluded-Worker filtering.
- Built with JDK 8 and deployed to a four-Worker Celeborn cluster.
- Ran an E2E shuffle that started with one Worker, added three Workers, and
paused the original Worker. The client detected the failure, revived partitions
on new Workers, and successfully validated 8 million rows with fallback
disabled.
- Restored all Workers and reran the standard Spark/YARN smoke test
successfully.
Closes #3775 from Kalvin2077/feat/dynamic-resource.
Authored-by: Kalvin2077 <[email protected]>
Signed-off-by: zhengtao <[email protected]>
AI-Contributed/Feature: 0/471
AI-Contributed/UT: 0/431
---
.../celeborn/client/ChangePartitionManager.scala | 98 +++--------
.../apache/celeborn/client/LifecycleManager.scala | 146 ++++++++++++++-
.../celeborn/client/WorkerStatusTracker.scala | 15 ++
.../client/ChangePartitionManagerSuite.scala | 90 ++++++++++
.../celeborn/client/WorkerStatusTrackerSuite.scala | 25 +++
common/src/main/proto/TransportMessages.proto | 17 ++
.../org/apache/celeborn/common/CelebornConf.scala | 43 +++--
.../common/protocol/message/ControlMessages.scala | 12 ++
.../apache/celeborn/common/CelebornConfSuite.scala | 25 +++
.../apache/celeborn/common/util/UtilsSuite.scala | 33 +++-
docs/configuration/client.md | 4 +-
docs/configuration/master.md | 1 +
docs/migration.md | 5 +
.../celeborn/service/deploy/master/Master.scala | 130 +++++++++++---
.../service/deploy/master/MasterSuite.scala | 196 ++++++++++++++++++++-
.../ChangePartitionManagerUpdateWorkersSuite.scala | 60 +++++--
.../celeborn/tests/spark/RetryReviveTest.scala | 2 +-
17 files changed, 760 insertions(+), 142 deletions(-)
diff --git
a/client/src/main/scala/org/apache/celeborn/client/ChangePartitionManager.scala
b/client/src/main/scala/org/apache/celeborn/client/ChangePartitionManager.scala
index 71096a952f..c0af4e9113 100644
---
a/client/src/main/scala/org/apache/celeborn/client/ChangePartitionManager.scala
+++
b/client/src/main/scala/org/apache/celeborn/client/ChangePartitionManager.scala
@@ -23,7 +23,6 @@ import java.util.concurrent.{ConcurrentHashMap,
ScheduledExecutorService, Schedu
import scala.collection.JavaConverters._
-import org.apache.celeborn.client.LifecycleManager.ShuffleFailedWorkers
import org.apache.celeborn.common.CelebornConf
import org.apache.celeborn.common.internal.Logging
import org.apache.celeborn.common.meta.{ShufflePartitionLocationInfo,
WorkerInfo}
@@ -77,7 +76,6 @@ class ChangePartitionManager(
private val testRetryRevive = conf.testRetryRevive
private val dynamicResourceEnabled = conf.clientShuffleDynamicResourceEnabled
- private val dynamicResourceUnavailableFactor =
conf.clientShuffleDynamicResourceFactor
def start(): Unit = {
batchHandleChangePartition = batchHandleChangePartitionSchedulerThread.map
{
@@ -284,73 +282,7 @@ class ChangePartitionManager(
}
}
- val candidates = new util.HashSet[WorkerInfo]()
- val newlyRequestedLocations = new WorkerResource()
-
- val snapshotCandidates =
- lifecycleManager
- .workerSnapshots(shuffleId)
- .asScala
- .values
- .map(_.workerInfo)
- .filter(lifecycleManager.workerStatusTracker.workerAvailable)
- .toSet
- .asJava
- candidates.addAll(snapshotCandidates)
-
- if (dynamicResourceEnabled) {
- val shuffleAllocatedWorkers =
lifecycleManager.workerSnapshots(shuffleId).size()
- val unavailableWorkerRatio = 1 - (snapshotCandidates.size * 1.0 /
shuffleAllocatedWorkers)
- if (candidates.size < 1 || (pushReplicateEnabled && candidates.size < 2)
- || (unavailableWorkerRatio >= dynamicResourceUnavailableFactor)) {
-
- // get new available workers for the request partition ids
- val partitionIds = new util.ArrayList[Integer](
-
changePartitions.map(_.partitionId).map(Integer.valueOf).toList.asJava)
- // The partition id value is not important here because we're just
trying to get the workers to use
- val requestSlotsRes =
- lifecycleManager.requestMasterRequestSlotsWithRetry(shuffleId,
partitionIds)
-
- requestSlotsRes.status match {
- case StatusCode.REQUEST_FAILED =>
- logInfo(s"ChangePartition requestSlots RPC request failed for
$shuffleId!")
- case StatusCode.SLOT_NOT_AVAILABLE =>
- logInfo(s"ChangePartition requestSlots for $shuffleId failed, have
no available slots.")
- case StatusCode.SUCCESS =>
- logDebug(
- s"ChangePartition requestSlots request for workers Success!
shuffleId: $shuffleId availableWorkers Info:
${requestSlotsRes.workerResource.keySet()}")
- case StatusCode.WORKER_EXCLUDED =>
- logInfo(s"ChangePartition requestSlots request for workers for
$shuffleId failed due to all workers be excluded!")
- case _ => // won't happen
- throw new UnsupportedOperationException()
- }
-
- if (requestSlotsRes.status.equals(StatusCode.SUCCESS)) {
- requestSlotsRes.workerResource.keySet().asScala.foreach {
workerInfo: WorkerInfo =>
- newlyRequestedLocations.computeIfAbsent(workerInfo,
lifecycleManager.newLocationFunc)
- }
-
- // SetupEndpoint for new Workers
- val workersRequireEndpoints = new util.HashSet[WorkerInfo](
- requestSlotsRes.workerResource.keySet()
- .asScala
- .filter(lifecycleManager.workerStatusTracker.workerAvailable)
- .asJava)
-
- val connectFailedWorkers = new ShuffleFailedWorkers()
- lifecycleManager.setupEndpoints(
- workersRequireEndpoints,
- shuffleId,
- connectFailedWorkers)
-
workersRequireEndpoints.removeAll(connectFailedWorkers.asScala.keys.toList.asJava)
- candidates.addAll(workersRequireEndpoints)
-
- // Update worker status
-
lifecycleManager.workerStatusTracker.recordWorkerFailure(connectFailedWorkers)
-
lifecycleManager.workerStatusTracker.removeFromExcludedWorkers(candidates)
- }
- }
- }
+ val candidates = collectCandidateWorkers(shuffleId)
if (candidates.size < 1 || (pushReplicateEnabled && candidates.size < 2)) {
logError("[Update partition] failed for not enough candidates for
revive.")
@@ -374,10 +306,7 @@ class ChangePartitionManager(
return
}
- // newlyRequestedLocations is empty if dynamicResourceEnabled is false
- newlyRequestedLocations.putAll(newlyAllocatedLocations)
-
- val newPrimaryLocations = newlyRequestedLocations.asScala.flatMap {
+ val newPrimaryLocations = newlyAllocatedLocations.asScala.flatMap {
case (workInfo, (primaryLocations, replicaLocations)) =>
// Add all re-allocated slots to worker snapshots.
val partitionLocationInfo =
lifecycleManager.workerSnapshots(shuffleId).computeIfAbsent(
@@ -410,6 +339,29 @@ class ChangePartitionManager(
replySuccess(newPrimaryLocations.toArray)
}
+ private[client] def collectCandidateWorkers(shuffleId: Int):
util.HashSet[WorkerInfo] = {
+ if (dynamicResourceEnabled) {
+ lifecycleManager.refreshEndpointReadyWorkersFromMaster(shuffleId)
+ }
+
+ val snapshotCandidates =
+ lifecycleManager
+ .workerSnapshots(shuffleId)
+ .asScala
+ .values
+ .map(_.workerInfo)
+ .filter(lifecycleManager.workerStatusTracker.workerAvailable)
+ .toSet
+ val candidates = new util.HashSet[WorkerInfo](snapshotCandidates.asJava)
+ if (dynamicResourceEnabled) {
+ candidates.addAll(
+ lifecycleManager.workerStatusTracker.endpointReadyWorkers
+ .filter(lifecycleManager.workerStatusTracker.workerAvailable)
+ .asJava)
+ }
+ candidates
+ }
+
private def reallocateChangePartitionRequestSlotsFromCandidates(
changePartitionRequests: List[ChangePartitionRequest],
candidates: List[WorkerInfo]): WorkerResource = {
diff --git
a/client/src/main/scala/org/apache/celeborn/client/LifecycleManager.scala
b/client/src/main/scala/org/apache/celeborn/client/LifecycleManager.scala
index f9508cfe6c..88b778395f 100644
--- a/client/src/main/scala/org/apache/celeborn/client/LifecycleManager.scala
+++ b/client/src/main/scala/org/apache/celeborn/client/LifecycleManager.scala
@@ -124,6 +124,11 @@ class LifecycleManager(val appUniqueId: String, val conf:
CelebornConf) extends
private val excludedWorkersFilter =
conf.registerShuffleFilterExcludedWorkerEnabled
+ private val dynamicResourceUpdateTime =
conf.clientShuffleDynamicResourceUpdateTime
+ private val endpointReadyWorkersRefreshLock = new Object
+ private var endpointReadyWorkersRefreshInProgress = false
+ private var lastEndpointReadyWorkersRefreshAttemptTime = 0L
+
private val registerShuffleResponseRpcCache: Cache[Int, ByteBuffer] =
CacheBuilder.newBuilder()
.concurrencyLevel(rpcCacheConcurrencyLevel)
.expireAfterAccess(rpcCacheExpireTime, TimeUnit.MILLISECONDS)
@@ -859,6 +864,7 @@ class LifecycleManager(val appUniqueId: String, val conf:
CelebornConf) extends
partitionLocationInfo.addReplicaPartitions(replicaLocations)
allocatedWorkers.put(workerInfo.toUniqueId, partitionLocationInfo)
}
+
workerStatusTracker.addEndpointReadyWorkers(candidatesWorkers.asScala.toSet)
shuffleAllocatedWorkers.put(shuffleId, allocatedWorkers)
registeredShuffle.add(shuffleId)
commitManager.registerShuffle(
@@ -1850,12 +1856,7 @@ class LifecycleManager(val appUniqueId: String, val
conf: CelebornConf) extends
def requestMasterRequestSlotsWithRetry(
shuffleId: Int,
ids: util.ArrayList[Integer]): RequestSlotsResponse = {
- val excludedWorkerSet =
- if (excludedWorkersFilter) {
- workerStatusTracker.excludedWorkers.asScala.keys.toSet
- } else {
- Set.empty[WorkerInfo]
- }
+ val excludedWorkerSet = currentExcludedWorkerSet
// UserResourceConsumption and DiskInfo are eliminated from WorkerInfo
// during serialization of RequestSlots
val req =
@@ -1880,6 +1881,139 @@ class LifecycleManager(val appUniqueId: String, val
conf: CelebornConf) extends
}
}
+ private def syncEndpointReadyWorkers(
+ shuffleId: Int,
+ workersFromMaster: Set[WorkerInfo]): Unit = {
+ val currentEndpointReadyWorkers = workerStatusTracker.endpointReadyWorkers
+ val workersToRemove = currentEndpointReadyWorkers.diff(workersFromMaster)
+ val workersToConnect = workersFromMaster.diff(currentEndpointReadyWorkers)
+ val connectFailedWorkers = new ShuffleFailedWorkers()
+ setupEndpoints(workersToConnect.asJava, shuffleId, connectFailedWorkers)
+ workerStatusTracker.recordWorkerFailure(connectFailedWorkers)
+
+ val connectedWorkers =
workersToConnect.diff(connectFailedWorkers.asScala.keySet)
+ workerStatusTracker.addEndpointReadyWorkers(connectedWorkers)
+ workerStatusTracker.removeEndpointReadyWorkers(workersToRemove)
+ }
+
+ private[client] def refreshEndpointReadyWorkersFromMaster(shuffleId: Int):
Unit = {
+ val shouldRefresh = endpointReadyWorkersRefreshLock.synchronized {
+ var waitedForRefresh = false
+ val waitDeadline = System.currentTimeMillis() + rpcAskTimeoutMs
+ var remainingWaitTime = rpcAskTimeoutMs
+ while (endpointReadyWorkersRefreshInProgress && remainingWaitTime > 0) {
+ // Reuse the in-flight result instead of letting this revive observe
an incomplete pool.
+ waitedForRefresh = true
+ try {
+ endpointReadyWorkersRefreshLock.wait(remainingWaitTime)
+ } catch {
+ case _: InterruptedException =>
+ Thread.currentThread().interrupt()
+ return
+ }
+ remainingWaitTime = waitDeadline - System.currentTimeMillis()
+ }
+ if (endpointReadyWorkersRefreshInProgress) {
+ logWarning(
+ s"Timed out after ${rpcAskTimeoutMs}ms waiting for the in-flight
endpoint-ready " +
+ "workers refresh; continue using the current worker pool.")
+ }
+
+ val currentTime = System.currentTimeMillis()
+ val refreshIntervalElapsed = lastEndpointReadyWorkersRefreshAttemptTime
== 0L ||
+ currentTime - lastEndpointReadyWorkersRefreshAttemptTime >=
dynamicResourceUpdateTime
+ if (!waitedForRefresh && refreshIntervalElapsed) {
+ endpointReadyWorkersRefreshInProgress = true
+ true
+ } else {
+ false
+ }
+ }
+ if (!shouldRefresh) {
+ return
+ }
+
+ try {
+ val requestWorkersRes = requestMasterRequestWorkersWithRetry()
+ StatusCode.fromValue(requestWorkersRes.getStatus) match {
+ case StatusCode.REQUEST_FAILED =>
+ logInfo("ChangePartition requestWorkers RPC request failed.")
+ case StatusCode.SUCCESS =>
+ val availableWorkers =
+ requestWorkersRes.getWorkersList.asScala.map { pbWorkerInfo =>
+ val workerInfo = PbSerDeUtils.fromPbWorkerInfo(pbWorkerInfo)
+ if (pbWorkerInfo.getNetworkLocation.nonEmpty) {
+ workerInfo.networkLocation = pbWorkerInfo.getNetworkLocation
+ }
+ workerInfo
+ }.toSet
+ syncEndpointReadyWorkers(shuffleId, availableWorkers)
+ logDebug(
+ s"ChangePartition requestWorkers succeeded with workers " +
+ s"$availableWorkers.")
+ case StatusCode.WORKER_EXCLUDED =>
+ syncEndpointReadyWorkers(shuffleId, Set.empty)
+ logInfo(s"Offer workers for appId $appUniqueId shuffleId $shuffleId
failed.")
+ case StatusCode.SLOT_NOT_AVAILABLE =>
+ syncEndpointReadyWorkers(shuffleId, Set.empty)
+ logInfo(
+ s"No eligible workers are available for appId $appUniqueId
shuffleId $shuffleId.")
+ case status =>
+ logWarning(
+ s"ChangePartition requestWorkers failed with status $status.")
+ }
+ } finally {
+ endpointReadyWorkersRefreshLock.synchronized {
+ lastEndpointReadyWorkersRefreshAttemptTime = System.currentTimeMillis()
+ endpointReadyWorkersRefreshInProgress = false
+ endpointReadyWorkersRefreshLock.notifyAll()
+ }
+ }
+ }
+
+ private def requestMasterRequestWorkersWithRetry(): PbRequestWorkersResponse
= {
+ val excludedWorkerSet = currentExcludedWorkerSet
+ val req = PbRequestWorkers.newBuilder()
+ .setApplicationId(appUniqueId)
+ .setUserIdentifier(PbSerDeUtils.toPbUserIdentifier(userIdentifier))
+ .setMaxWorkers(slotsAssignMaxWorkers)
+ .setTagsExpr(clientTagsExpr)
+ .setShouldReplicate(pushReplicateEnabled)
+ .setAvailableStorageTypes(availableStorageTypes)
+ .addAllExcludedWorkerSet(excludedWorkerSet.map(
+ PbSerDeUtils.toPbWorkerInfo(_, true, true)).asJava)
+ .build()
+ val res = requestMasterRequestWorkers(req)
+ if (StatusCode.fromValue(res.getStatus) == StatusCode.REQUEST_FAILED) {
+ requestMasterRequestWorkers(req)
+ } else {
+ res
+ }
+ }
+
+ private def currentExcludedWorkerSet: Set[WorkerInfo] = {
+ if (excludedWorkersFilter) {
+ workerStatusTracker.excludedWorkers.asScala.keys.toSet
+ } else {
+ Set.empty
+ }
+ }
+
+ private def requestMasterRequestWorkers(
+ message: PbRequestWorkers): PbRequestWorkersResponse = {
+ try {
+ masterClient.askSync[PbRequestWorkersResponse](
+ message,
+ classOf[PbRequestWorkersResponse])
+ } catch {
+ case e: Exception =>
+ logError("AskSync request workers failed.", e)
+ PbRequestWorkersResponse.newBuilder()
+ .setStatus(StatusCode.REQUEST_FAILED.getValue)
+ .build()
+ }
+ }
+
private def requestMasterRequestSlots(message: RequestSlots):
RequestSlotsResponse = {
val shuffleKey = Utils.makeShuffleKey(message.applicationId,
message.shuffleId)
try {
diff --git
a/client/src/main/scala/org/apache/celeborn/client/WorkerStatusTracker.scala
b/client/src/main/scala/org/apache/celeborn/client/WorkerStatusTracker.scala
index 2c1243acb1..67e56a08e1 100644
--- a/client/src/main/scala/org/apache/celeborn/client/WorkerStatusTracker.scala
+++ b/client/src/main/scala/org/apache/celeborn/client/WorkerStatusTracker.scala
@@ -41,6 +41,21 @@ class WorkerStatusTracker(
val excludedWorkers = new ShuffleFailedWorkers()
val shuttingWorkers: JSet[WorkerInfo] =
ConcurrentHashMap.newKeySet[WorkerInfo]()
+ // These workers already have client-local RPC endpoints and can reserve
slots directly.
+ private val endpointReadyWorkerSet: JSet[WorkerInfo] =
+ ConcurrentHashMap.newKeySet[WorkerInfo]()
+
+ private[client] def endpointReadyWorkers: Set[WorkerInfo] =
+ endpointReadyWorkerSet.asScala.toSet
+
+ private[client] def addEndpointReadyWorkers(workers: Set[WorkerInfo]): Unit
= {
+ workers.filter(workerAvailable).foreach(endpointReadyWorkerSet.add)
+ }
+
+ private[client] def removeEndpointReadyWorkers(workers: Set[WorkerInfo]):
Unit = {
+ workers.foreach(endpointReadyWorkerSet.remove)
+ }
+
def registerWorkerStatusListener(workerStatusListener:
WorkerStatusListener): Unit = {
workerStatusListeners.add(workerStatusListener)
}
diff --git
a/client/src/test/scala/org/apache/celeborn/client/ChangePartitionManagerSuite.scala
b/client/src/test/scala/org/apache/celeborn/client/ChangePartitionManagerSuite.scala
new file mode 100644
index 0000000000..f57d1499ed
--- /dev/null
+++
b/client/src/test/scala/org/apache/celeborn/client/ChangePartitionManagerSuite.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.celeborn.client
+
+import java.util
+
+import scala.collection.JavaConverters._
+
+import org.mockito.Mockito.{doAnswer, mock, verify, when}
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.stubbing.Answer
+
+import org.apache.celeborn.CelebornFunSuite
+import org.apache.celeborn.common.CelebornConf
+import org.apache.celeborn.common.meta.{ShufflePartitionLocationInfo,
WorkerInfo}
+import org.apache.celeborn.common.protocol.message.StatusCode
+
+class ChangePartitionManagerSuite extends CelebornFunSuite {
+ test("collectCandidateWorkers combines snapshot and endpoint-ready workers")
{
+ val conf = dynamicResourceConf()
+ val lifecycleManager = mock(classOf[LifecycleManager])
+ val statusTracker = new WorkerStatusTracker(conf, lifecycleManager)
+ when(lifecycleManager.workerStatusTracker).thenReturn(statusTracker)
+
+ val snapshotWorker = worker("snapshot")
+ val duplicateSnapshotWorker = worker("snapshot")
+ val dynamicWorker = worker("dynamic")
+
when(lifecycleManager.workerSnapshots(1)).thenReturn(snapshot(snapshotWorker))
+ statusTracker.addEndpointReadyWorkers(Set(duplicateSnapshotWorker,
dynamicWorker))
+
+ val manager = new ChangePartitionManager(conf, lifecycleManager)
+ val candidates = manager.collectCandidateWorkers(1)
+
+ verify(lifecycleManager).refreshEndpointReadyWorkersFromMaster(1)
+ assert(candidates.asScala.toSet === Set(snapshotWorker, dynamicWorker))
+ assert(candidates.asScala.find(_ == snapshotWorker).get eq snapshotWorker)
+ }
+
+ test("collectCandidateWorkers filters workers excluded during refresh") {
+ val conf = dynamicResourceConf()
+ val lifecycleManager = mock(classOf[LifecycleManager])
+ val statusTracker = new WorkerStatusTracker(conf, lifecycleManager)
+ when(lifecycleManager.workerStatusTracker).thenReturn(statusTracker)
+
+ val failedWorker = worker("failed")
+
when(lifecycleManager.workerSnapshots(1)).thenReturn(snapshot(failedWorker))
+ statusTracker.addEndpointReadyWorkers(Set(failedWorker))
+ doAnswer(new Answer[AnyRef] {
+ override def answer(invocation: InvocationOnMock): AnyRef = {
+ statusTracker.excludedWorkers.put(
+ failedWorker,
+ (StatusCode.WORKER_UNRESPONSIVE, System.currentTimeMillis()))
+ null
+ }
+ }).when(lifecycleManager).refreshEndpointReadyWorkersFromMaster(1)
+
+ val manager = new ChangePartitionManager(conf, lifecycleManager)
+
+ assert(manager.collectCandidateWorkers(1).isEmpty)
+ }
+
+ private def dynamicResourceConf(): CelebornConf = {
+ new
CelebornConf().set(CelebornConf.CLIENT_SHUFFLE_DYNAMIC_RESOURCE_ENABLED, true)
+ }
+
+ private def worker(host: String): WorkerInfo = {
+ new WorkerInfo(host, 1001, 1002, 1003, 1004)
+ }
+
+ private def snapshot(worker: WorkerInfo): util.Map[String,
ShufflePartitionLocationInfo] = {
+ val workers = new util.HashMap[String, ShufflePartitionLocationInfo]()
+ workers.put(worker.toUniqueId, new ShufflePartitionLocationInfo(worker))
+ workers
+ }
+}
diff --git
a/client/src/test/scala/org/apache/celeborn/client/WorkerStatusTrackerSuite.scala
b/client/src/test/scala/org/apache/celeborn/client/WorkerStatusTrackerSuite.scala
index 90a1d53bbd..43cbefd45e 100644
---
a/client/src/test/scala/org/apache/celeborn/client/WorkerStatusTrackerSuite.scala
+++
b/client/src/test/scala/org/apache/celeborn/client/WorkerStatusTrackerSuite.scala
@@ -34,6 +34,31 @@ import
org.apache.celeborn.common.protocol.message.ControlMessages.HeartbeatFrom
import org.apache.celeborn.common.protocol.message.StatusCode
class WorkerStatusTrackerSuite extends CelebornFunSuite {
+ test("maintain endpoint-ready worker pool") {
+ val statusTracker = new WorkerStatusTracker(new CelebornConf(), null)
+ val worker1 = mock("host1")
+ val worker2 = mock("host2")
+ val worker3 = mock("host3")
+
+ statusTracker.addEndpointReadyWorkers(Set(worker1, worker2))
+ assert(statusTracker.endpointReadyWorkers == Set(worker1, worker2))
+
+ val replacementWorker2 = mock("host2")
+ statusTracker.addEndpointReadyWorkers(Set(replacementWorker2, worker3))
+ assert(statusTracker.endpointReadyWorkers == Set(worker1, worker2,
worker3))
+ // Keep the existing WorkerInfo instance because it owns the client-local
RPC endpoint.
+ assert(statusTracker.endpointReadyWorkers.find(_ == worker2).get eq
worker2)
+
+ statusTracker.removeEndpointReadyWorkers(Set(worker1))
+ assert(statusTracker.endpointReadyWorkers == Set(worker2, worker3))
+
+ statusTracker.excludedWorkers.put(
+ worker1,
+ (StatusCode.WORKER_EXCLUDED, System.currentTimeMillis()))
+ statusTracker.addEndpointReadyWorkers(Set(worker1))
+ assert(statusTracker.endpointReadyWorkers == Set(worker2, worker3))
+ }
+
test("handleHeartbeatResponse without availableWorkers") {
val celebornConf = new CelebornConf()
celebornConf.set(CLIENT_EXCLUDED_WORKER_EXPIRE_TIMEOUT, 2000L)
diff --git a/common/src/main/proto/TransportMessages.proto
b/common/src/main/proto/TransportMessages.proto
index a813a9e501..e57055391e 100644
--- a/common/src/main/proto/TransportMessages.proto
+++ b/common/src/main/proto/TransportMessages.proto
@@ -117,6 +117,8 @@ enum MessageType {
READ_REDUCER_PARTITION_END = 94;
READ_REDUCER_PARTITION_END_RESPONSE = 95;
REGISTER_APPLICATION_INFO = 96;
+ REQUEST_WORKERS = 97;
+ REQUEST_WORKERS_RESPONSE = 98;
}
enum StreamType {
@@ -325,6 +327,16 @@ message PbRequestSlots {
string tagsExpr = 14;
}
+message PbRequestWorkers {
+ string applicationId = 1;
+ PbUserIdentifier userIdentifier = 2;
+ int32 maxWorkers = 3;
+ string tagsExpr = 4;
+ repeated PbWorkerInfo excludedWorkerSet = 5;
+ bool shouldReplicate = 6;
+ int32 availableStorageTypes = 7;
+}
+
message PbSlotInfo {
map<string, int32> slot = 1;
}
@@ -335,6 +347,11 @@ message PbRequestSlotsResponse {
map<string, PbPackedWorkerResource> packedWorkerResource = 3;
}
+message PbRequestWorkersResponse {
+ int32 status = 1;
+ repeated PbWorkerInfo workers = 2;
+}
+
message PbRevivePartitionInfo {
int32 partitionId = 1;
int32 epoch = 2;
diff --git
a/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
b/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
index 2a928c611b..1e34ac34f9 100644
--- a/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
+++ b/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
@@ -659,7 +659,7 @@ class CelebornConf(loadDefaults: Boolean) extends Cloneable
with Logging with Se
def masterSlotAssignPolicyName: String = get(MASTER_SLOT_ASSIGN_POLICY)
/** Returns the configured built-in policy. Use `masterSlotAssignPolicyName`
for SPI providers. */
- @deprecated("Use masterSlotAssignPolicyName for SPI provider selection",
"0.7.0")
+ @deprecated("Use masterSlotAssignPolicyName for SPI provider selection",
"1.0.0")
def masterSlotAssignPolicy: SlotsAssignPolicy =
SlotsAssignPolicy.valueOf(get(MASTER_SLOT_ASSIGN_POLICY))
@@ -688,6 +688,7 @@ class CelebornConf(loadDefaults: Boolean) extends Cloneable
with Logging with Se
def masterSlotAssignExtraSlots: Int = get(MASTER_SLOT_ASSIGN_EXTRA_SLOTS)
def masterSlotAssignMaxWorkers: Int = get(MASTER_SLOT_ASSIGN_MAX_WORKERS)
def masterSlotAssignMinWorkers: Int = get(MASTER_SLOT_ASSIGN_MIN_WORKERS)
+ def masterSplitSlotAssignMaxWorkers: Int =
get(MASTER_SPLIT_SLOT_ASSIGN_MAX_WORKERS)
def initialEstimatedPartitionSize: Long =
get(ESTIMATED_PARTITION_SIZE_INITIAL_SIZE)
def estimatedPartitionSizeUpdaterInitialDelay: Long =
get(ESTIMATED_PARTITION_SIZE_UPDATE_INITIAL_DELAY)
@@ -961,7 +962,8 @@ class CelebornConf(loadDefaults: Boolean) extends Cloneable
with Logging with Se
def clientCommitFilesIgnoreExcludedWorkers: Boolean =
get(CLIENT_COMMIT_IGNORE_EXCLUDED_WORKERS)
def clientShuffleDynamicResourceEnabled: Boolean =
get(CLIENT_SHUFFLE_DYNAMIC_RESOURCE_ENABLED)
- def clientShuffleDynamicResourceFactor: Double =
get(CLIENT_SHUFFLE_DYNAMIC_RESOURCE_FACTOR)
+ def clientShuffleDynamicResourceUpdateTime: Long =
+ get(CLIENT_SHUFFLE_DYNAMIC_RESOURCE_UPDATE_TIME)
def appHeartbeatTimeoutMs: Long = get(APPLICATION_HEARTBEAT_TIMEOUT)
def dfsExpireDirsTimeoutMS: Long = get(DFS_EXPIRE_DIRS_TIMEOUT)
def appHeartbeatIntervalMs: Long = get(APPLICATION_HEARTBEAT_INTERVAL)
@@ -3222,6 +3224,18 @@ object CelebornConf extends Logging {
.intConf
.createWithDefault(100)
+ val MASTER_SPLIT_SLOT_ASSIGN_MAX_WORKERS: ConfigEntry[Int] =
+ buildConf("celeborn.master.splitSlot.assign.maxWorkers")
+ .categories("master")
+ .version("1.0.0")
+ .doc("Maximum workers returned by each dynamic candidate refresh. The
request limit is the " +
+ "smaller positive value of this setting and
`celeborn.client.slot.assign.maxWorkers`. " +
+ "For replicated shuffle, an effective limit of one is raised to two.
Workers already " +
+ "present in a shuffle snapshot are not counted against this limit.")
+ .intConf
+ .checkValue(_ > 0, "Must be positive.")
+ .createWithDefault(500)
+
val ESTIMATED_PARTITION_SIZE_INITIAL_SIZE: ConfigEntry[Long] =
buildConf("celeborn.master.estimatedPartitionSize.initialSize")
.withAlternative("celeborn.shuffle.initialEstimatedPartitionSize")
@@ -5603,21 +5617,24 @@ object CelebornConf extends Logging {
buildConf("celeborn.client.shuffle.dynamicResourceEnabled")
.categories("client")
.version("0.6.0")
- .doc("When enabled, the ChangePartitionManager will obtain candidate
workers from the availableWorkers pool " +
- "during heartbeats when worker resource change.")
+ .doc("When enabled, ChangePartitionManager refreshes endpoint-ready
worker candidates from " +
+ "the Master on demand while handling change-partition requests, and
combines them with " +
+ "workers already present in the shuffle snapshot.")
.booleanConf
.createWithDefault(false)
- val CLIENT_SHUFFLE_DYNAMIC_RESOURCE_FACTOR: ConfigEntry[Double] =
- buildConf("celeborn.client.shuffle.dynamicResourceFactor")
+ val CLIENT_SHUFFLE_DYNAMIC_RESOURCE_UPDATE_TIME: ConfigEntry[Long] =
+ buildConf("celeborn.client.shuffle.dynamicResource.updateTime")
.categories("client")
- .version("0.6.0")
- .doc("The ChangePartitionManager will check whether (unavailable workers
/ shuffle allocated workers) " +
- "is more than the factor before obtaining candidate workers from the
requestSlots RPC response " +
- s"when `${CLIENT_SHUFFLE_DYNAMIC_RESOURCE_ENABLED.key}` set true")
- .doubleConf
- .checkValue(v => v >= 0.0 && v <= 1.0, "Should be in [0.0, 1.0].")
- .createWithDefault(0.5)
+ .version("1.0.0")
+ .doc(
+ "Minimum interval after a worker-candidate refresh attempt completes
before " +
+ s"ChangePartitionManager may try again when
`${CLIENT_SHUFFLE_DYNAMIC_RESOURCE_ENABLED.key}` " +
+ "is true. Set to 0 to allow each change-partition handling cycle to
refresh when no " +
+ "refresh is already in progress.")
+ .timeConf(TimeUnit.MILLISECONDS)
+ .checkValue(_ >= 0, "Must be non-negative.")
+ .createWithDefaultString("30s")
val CLIENT_PUSH_STAGE_END_TIMEOUT: ConfigEntry[Long] =
buildConf("celeborn.client.push.stageEnd.timeout")
diff --git
a/common/src/main/scala/org/apache/celeborn/common/protocol/message/ControlMessages.scala
b/common/src/main/scala/org/apache/celeborn/common/protocol/message/ControlMessages.scala
index f1e34aa54f..b305539b65 100644
---
a/common/src/main/scala/org/apache/celeborn/common/protocol/message/ControlMessages.scala
+++
b/common/src/main/scala/org/apache/celeborn/common/protocol/message/ControlMessages.scala
@@ -679,6 +679,9 @@ object ControlMessages extends Logging {
.build().toByteArray
new TransportMessage(MessageType.REQUEST_SLOTS, payload)
+ case pb: PbRequestWorkers =>
+ new TransportMessage(MessageType.REQUEST_WORKERS, pb.toByteArray)
+
case RequestSlotsResponse(status, workerResource, packed) =>
val builder = PbRequestSlotsResponse.newBuilder()
.setStatus(status.getValue)
@@ -693,6 +696,9 @@ object ControlMessages extends Logging {
val payload = builder.build().toByteArray
new TransportMessage(MessageType.REQUEST_SLOTS_RESPONSE, payload)
+ case pb: PbRequestWorkersResponse =>
+ new TransportMessage(MessageType.REQUEST_WORKERS_RESPONSE,
pb.toByteArray)
+
case Revive(shuffleId, mapIds, reviveRequests, serdeVersion) =>
val builder = PbRevive.newBuilder()
.setShuffleId(shuffleId)
@@ -1177,6 +1183,12 @@ object ControlMessages extends Logging {
StatusCode.fromValue(pbRequestSlotsResponse.getStatus),
workerResource)
+ case REQUEST_WORKERS_VALUE =>
+ PbRequestWorkers.parseFrom(message.getPayload)
+
+ case REQUEST_WORKERS_RESPONSE_VALUE =>
+ PbRequestWorkersResponse.parseFrom(message.getPayload)
+
case CHANGE_LOCATION_VALUE =>
val pbRevive = PbRevive.parseFrom(message.getPayload)
val shuffleId = pbRevive.getShuffleId
diff --git
a/common/src/test/scala/org/apache/celeborn/common/CelebornConfSuite.scala
b/common/src/test/scala/org/apache/celeborn/common/CelebornConfSuite.scala
index 18d71e23eb..392d5edcda 100644
--- a/common/src/test/scala/org/apache/celeborn/common/CelebornConfSuite.scala
+++ b/common/src/test/scala/org/apache/celeborn/common/CelebornConfSuite.scala
@@ -24,6 +24,31 @@ import org.apache.celeborn.common.protocol.StorageInfo
class CelebornConfSuite extends CelebornFunSuite {
+ test("master split slot assign max workers must be positive") {
+ Seq("0", "-1").foreach { value =>
+ val error = intercept[IllegalArgumentException] {
+ new CelebornConf()
+ .set(MASTER_SPLIT_SLOT_ASSIGN_MAX_WORKERS.key, value)
+ .masterSplitSlotAssignMaxWorkers
+ }
+ assert(error.getMessage.contains("Must be positive."))
+ }
+ }
+
+ test("dynamic resource update time must be non-negative") {
+ assert(
+ new CelebornConf()
+ .set(CLIENT_SHUFFLE_DYNAMIC_RESOURCE_UPDATE_TIME.key, "0")
+ .clientShuffleDynamicResourceUpdateTime == 0L)
+
+ val error = intercept[IllegalArgumentException] {
+ new CelebornConf()
+ .set(CLIENT_SHUFFLE_DYNAMIC_RESOURCE_UPDATE_TIME.key, "-1ms")
+ .clientShuffleDynamicResourceUpdateTime
+ }
+ assert(error.getMessage.contains("Must be non-negative."))
+ }
+
test("JVMQuake thresholds should preserve configured time units") {
val conf = new CelebornConf()
.set(WORKER_JVM_QUAKE_DUMP_THRESHOLD.key, "30s")
diff --git
a/common/src/test/scala/org/apache/celeborn/common/util/UtilsSuite.scala
b/common/src/test/scala/org/apache/celeborn/common/util/UtilsSuite.scala
index c98f3bd498..a3e03f7c9b 100644
--- a/common/src/test/scala/org/apache/celeborn/common/util/UtilsSuite.scala
+++ b/common/src/test/scala/org/apache/celeborn/common/util/UtilsSuite.scala
@@ -29,9 +29,10 @@ import org.apache.celeborn.CelebornFunSuite
import org.apache.celeborn.common.CelebornConf
import org.apache.celeborn.common.client.{MasterEndpointResolver,
StaticMasterEndpointResolver}
import org.apache.celeborn.common.exception.CelebornException
-import org.apache.celeborn.common.identity.DefaultIdentityProvider
+import org.apache.celeborn.common.identity.{DefaultIdentityProvider,
UserIdentifier}
+import org.apache.celeborn.common.meta.WorkerInfo
import org.apache.celeborn.common.network.protocol.SerdeVersion
-import org.apache.celeborn.common.protocol.{PartitionLocation,
PbReviseLostShuffles, PbReviseLostShufflesResponse, TransportModuleConstants}
+import org.apache.celeborn.common.protocol.{PartitionLocation,
PbRequestWorkers, PbRequestWorkersResponse, PbReviseLostShuffles,
PbReviseLostShufflesResponse, StorageInfo, TransportModuleConstants}
import
org.apache.celeborn.common.protocol.message.ControlMessages.{GetReducerFileGroupResponse,
MapperEnd, ReviseLostShuffles, ReviseLostShufflesResponse}
import org.apache.celeborn.common.protocol.message.StatusCode
@@ -197,6 +198,34 @@ class UtilsSuite extends CelebornFunSuite {
mapperEnd.bytesWrittenPerPartition.array should contain
theSameElementsInOrderAs mapperEndTrans.bytesWrittenPerPartition
}
+ test("PbRequestWorkers messages convert with TransportMessage") {
+ val excludedWorker = new WorkerInfo("host1", 1001, 1002, 1003, 1004)
+ val request = PbRequestWorkers.newBuilder()
+ .setApplicationId("app-1")
+ .setUserIdentifier(
+ PbSerDeUtils.toPbUserIdentifier(new UserIdentifier("tenant", "user")))
+ .setMaxWorkers(10)
+ .setTagsExpr("tag-a,tag-b")
+ .setShouldReplicate(true)
+ .setAvailableStorageTypes(StorageInfo.LOCAL_DISK_MASK)
+ .addExcludedWorkerSet(PbSerDeUtils.toPbWorkerInfo(excludedWorker, true,
true))
+ .build()
+ val convertedRequest =
+
Utils.fromTransportMessage(Utils.toTransportMessage(request)).asInstanceOf[PbRequestWorkers]
+ assert(convertedRequest == request)
+
+ val response = PbRequestWorkersResponse.newBuilder()
+ .setStatus(StatusCode.SUCCESS.getValue)
+ .addWorkers(PbSerDeUtils.toPbWorkerInfo(excludedWorker, true,
true).toBuilder
+ .setNetworkLocation("/rack-1")
+ .build())
+ .build()
+ val convertedResponse =
+ Utils.fromTransportMessage(Utils.toTransportMessage(response))
+ .asInstanceOf[PbRequestWorkersResponse]
+ assert(convertedResponse == response)
+ }
+
test("ReviseLostShuffles class convert with pb") {
val req = ReviseLostShuffles("app-1", util.Arrays.asList[Integer](1, 2,
3), "req-1")
val reqTrans = Utils.fromTransportMessage(Utils.toTransportMessage(req))
diff --git a/docs/configuration/client.md b/docs/configuration/client.md
index be5422c236..dbb89561e4 100644
--- a/docs/configuration/client.md
+++ b/docs/configuration/client.md
@@ -111,8 +111,8 @@ license: |
| celeborn.client.shuffle.compression.codec | LZ4 | false | The codec used to
compress shuffle data. By default, Celeborn provides three codecs: `lz4`,
`zstd`, `none`. `none` means that shuffle compression is disabled. Since Flink
version 1.16, zstd is supported for Flink shuffle client. | 0.3.0 |
celeborn.shuffle.compression.codec,remote-shuffle.job.compression.codec |
| celeborn.client.shuffle.compression.zstd.level | 1 | false | Compression
level for Zstd compression codec, its value should be an integer between -5 and
22. Increasing the compression level will result in better compression at the
expense of more CPU and memory. | 0.3.0 |
celeborn.shuffle.compression.zstd.level |
| celeborn.client.shuffle.decompression.lz4.xxhash.instance |
<undefined> | false | Decompression XXHash instance for Lz4. Available
options: JNI, JAVASAFE, JAVAUNSAFE. | 0.3.2 | |
-| celeborn.client.shuffle.dynamicResourceEnabled | false | false | When
enabled, the ChangePartitionManager will obtain candidate workers from the
availableWorkers pool during heartbeats when worker resource change. | 0.6.0 |
|
-| celeborn.client.shuffle.dynamicResourceFactor | 0.5 | false | The
ChangePartitionManager will check whether (unavailable workers / shuffle
allocated workers) is more than the factor before obtaining candidate workers
from the requestSlots RPC response when
`celeborn.client.shuffle.dynamicResourceEnabled` set true | 0.6.0 | |
+| celeborn.client.shuffle.dynamicResource.updateTime | 30s | false | Minimum
interval after a worker-candidate refresh attempt completes before
ChangePartitionManager may try again when
`celeborn.client.shuffle.dynamicResourceEnabled` is true. Set to 0 to allow
each change-partition handling cycle to refresh when no refresh is already in
progress. | 1.0.0 | |
+| celeborn.client.shuffle.dynamicResourceEnabled | false | false | When
enabled, ChangePartitionManager refreshes endpoint-ready worker candidates from
the Master on demand while handling change-partition requests, and combines
them with workers already present in the shuffle snapshot. | 0.6.0 | |
| celeborn.client.shuffle.expired.checkInterval | 60s | false | Interval for
client to check expired shuffles. | 0.3.0 |
celeborn.shuffle.expired.checkInterval |
| celeborn.client.shuffle.integrityCheck.enabled | false | false | When
`true`, enables end-to-end integrity checks for Spark and Flink workloads. |
0.6.1 | |
| celeborn.client.shuffle.manager.port | 0 | false | Port used by the
LifecycleManager on the Driver. | 0.3.0 | celeborn.shuffle.manager.port |
diff --git a/docs/configuration/master.md b/docs/configuration/master.md
index a01bca3f69..86a4158668 100644
--- a/docs/configuration/master.md
+++ b/docs/configuration/master.md
@@ -85,6 +85,7 @@ license: |
| celeborn.master.slot.assign.maxWorkers | 10000 | false | Max workers that
slots of one shuffle can be allocated on. Will choose the smaller positive one
from Master side and Client side, see `celeborn.client.slot.assign.maxWorkers`.
| 0.3.1 | |
| celeborn.master.slot.assign.minWorkers | 100 | false | Min workers that
slots of one shuffle should be allocated on. Provided enough workers are
available. | 0.6.0 | |
| celeborn.master.slot.assign.policy | ROUNDROBIN | true | Policy for master
to assign slots. Built-in policies are roundrobin and loadaware. Additional
policies can be registered through the SlotsAssignStrategyProvider SPI.
Loadaware policy will be ignored when `HDFS` is enabled in
`celeborn.storage.availableTypes` | 0.3.0 | celeborn.slots.assign.policy |
+| celeborn.master.splitSlot.assign.maxWorkers | 500 | false | Maximum workers
returned by each dynamic candidate refresh. The request limit is the smaller
positive value of this setting and `celeborn.client.slot.assign.maxWorkers`.
For replicated shuffle, an effective limit of one is raised to two. Workers
already present in a shuffle snapshot are not counted against this limit. |
1.0.0 | |
| celeborn.master.userResourceConsumption.metrics.enabled | false | false |
Whether to enable resource consumption metrics. | 0.6.0 | |
| celeborn.master.userResourceConsumption.update.interval | 30s | false | Time
length for a window about compute user resource consumption. | 0.3.0 | |
| celeborn.master.workerUnavailableInfo.expireTimeout | 1800s | false | Worker
unavailable info would be cleared when the retention period is expired. Set -1
to disable the expiration. | 0.3.1 | |
diff --git a/docs/migration.md b/docs/migration.md
index b015ef3d86..7c6f654b7a 100644
--- a/docs/migration.md
+++ b/docs/migration.md
@@ -28,6 +28,11 @@ license: |
configuration. Existing `ROUNDROBIN` and `LOADAWARE` values remain supported
and require no
migration. See [Slots
allocation](./developers/slotsallocation.md#custom-strategies) for details.
+- Since 1.0.0, Celeborn removed
`celeborn.client.shuffle.dynamicResourceFactor`, which used the
+ unavailable-worker ratio to trigger a refresh. Dynamic worker refresh is now
performed on demand
+ while handling change-partition requests and throttled by
+ `celeborn.client.shuffle.dynamicResource.updateTime` (30s by default).
+
# Upgrading from 0.6 to 0.7
- Since 0.7.0, Celeborn removed `ReleaseSlots`.
diff --git
a/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala
b/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala
index ca1b67bee8..81a5027384 100644
---
a/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala
+++
b/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala
@@ -22,7 +22,7 @@ import java.net.BindException
import java.util
import java.util.{Map => JMap}
import java.util.Collections
-import java.util.concurrent.{ExecutorService, ScheduledFuture, TimeUnit}
+import java.util.concurrent.{ExecutorService, ScheduledFuture,
ThreadLocalRandom, TimeUnit}
import java.util.concurrent.atomic.AtomicBoolean
import java.util.function.ToLongFunction
@@ -206,6 +206,7 @@ private[celeborn] class Master(
private val tagsManager = new TagsManager(Option(configService))
private val slotsAssignMaxWorkers = conf.masterSlotAssignMaxWorkers
+ private val splitSlotAssignMaxWorkers = conf.masterSplitSlotAssignMaxWorkers
private val slotsAssignMinWorkers = conf.masterSlotAssignMinWorkers
private val slotsAssignExtraSlots = conf.masterSlotAssignExtraSlots
private val slotsAssignStrategyManager =
@@ -528,6 +529,11 @@ private[celeborn] class Master(
checkAuth(context, applicationId)
executeWithLeaderChecker(context, handleRequestSlots(context,
requestSlots))
+ case requestWorkers: PbRequestWorkers =>
+ logTrace(s"Received RequestWorkers request $requestWorkers.")
+ checkAuth(context, requestWorkers.getApplicationId)
+ executeWithLeaderChecker(context, handleRequestWorkers(context,
requestWorkers))
+
case pb: PbBatchUnregisterShuffles =>
val applicationId = pb.getAppId
val shuffleIds = pb.getShuffleIdsList.asScala.toList
@@ -953,22 +959,11 @@ private[celeborn] class Master(
return
}
- val numWorkers = Math.min(
- Math.max(
- if (requestSlots.shouldReplicate) 2 else 1,
- if (requestSlots.maxWorkers <= 0) slotsAssignMaxWorkers
- else Math.min(slotsAssignMaxWorkers, requestSlots.maxWorkers)),
- numAvailableWorkers)
- val startIndex = Random.nextInt(numAvailableWorkers)
- val selectedWorkers = new util.ArrayList[WorkerInfo](numWorkers)
- selectedWorkers.addAll(availableWorkers.subList(
- startIndex,
- Math.min(numAvailableWorkers, startIndex + numWorkers)))
- if (startIndex + numWorkers > numAvailableWorkers) {
- selectedWorkers.addAll(availableWorkers.subList(
- 0,
- startIndex + numWorkers - numAvailableWorkers))
- }
+ val selectedWorkers = selectWorkers(
+ availableWorkers,
+ requestSlots.maxWorkers,
+ slotsAssignMaxWorkers,
+ requestSlots.shouldReplicate)
// offer slots
val slots =
masterSource.sample(MasterSource.OFFER_SLOTS_TIME,
s"offerSlots-${Random.nextInt()}") {
@@ -1043,7 +1038,7 @@ private[celeborn] class Master(
s"extraSlots=$offerSlotsExtraSize"))
if (authEnabled) {
- pushApplicationMetaToWorkers(requestSlots, slots)
+ pushApplicationMetaToWorkers(requestSlots.applicationId, slots.keySet())
}
context.reply(RequestSlotsResponse(
StatusCode.SUCCESS,
@@ -1051,26 +1046,111 @@ private[celeborn] class Master(
requestSlots.packed))
}
+ private def selectWorkers(
+ candidates: util.List[WorkerInfo],
+ numSelectMax: Int,
+ numAssignMax: Int,
+ shouldReplicate: Boolean): util.List[WorkerInfo] = {
+ val numCandidates = candidates.size()
+ if (numCandidates == 0) {
+ return Collections.emptyList()
+ }
+
+ val numWorkers = Math.min(
+ Math.max(
+ if (shouldReplicate) 2 else 1,
+ if (numSelectMax <= 0) numAssignMax
+ else Math.min(numAssignMax, numSelectMax)),
+ numCandidates)
+ val startIndex = ThreadLocalRandom.current().nextInt(numCandidates)
+ val selectedWorkers = new util.ArrayList[WorkerInfo](numWorkers)
+ selectedWorkers.addAll(candidates.subList(
+ startIndex,
+ Math.min(numCandidates, startIndex + numWorkers)))
+ if (startIndex + numWorkers > numCandidates) {
+ selectedWorkers.addAll(candidates.subList(
+ 0,
+ startIndex + numWorkers - numCandidates))
+ }
+ selectedWorkers
+ }
+
+ def handleRequestWorkers(context: RpcCallContext, requestWorkers:
PbRequestWorkers): Unit = {
+
+ val excludedWorkerSet =
+ requestWorkers.getExcludedWorkerSetList.asScala
+ .map(PbSerDeUtils.fromPbWorkerInfo)
+ .toSet
+ var availableWorkers = workersAvailable(excludedWorkerSet)
+ if (conf.tagsEnabled) {
+ availableWorkers = tagsManager.getTaggedWorkers(
+ PbSerDeUtils.fromPbUserIdentifier(requestWorkers.getUserIdentifier),
+ requestWorkers.getTagsExpr,
+ availableWorkers)
+ }
+ if (availableWorkers.isEmpty) {
+ logWarning(
+ s"Offer workers for ${requestWorkers.getApplicationId} failed due to
no available workers.")
+ context.reply(PbRequestWorkersResponse.newBuilder()
+ .setStatus(StatusCode.WORKER_EXCLUDED.getValue)
+ .build())
+ return
+ }
+
+ val candidates = new util.ArrayList[WorkerInfo]()
+ availableWorkers.asScala
+ .filter { worker =>
+
!StorageInfo.localDiskAvailable(requestWorkers.getAvailableStorageTypes) ||
+ worker.haveDisk
+ }
+ .foreach(candidates.add)
+ val selectedWorkers = selectWorkers(
+ candidates,
+ requestWorkers.getMaxWorkers,
+ splitSlotAssignMaxWorkers,
+ requestWorkers.getShouldReplicate)
+ if (selectedWorkers.isEmpty) {
+ logWarning(
+ s"Offer workers for ${requestWorkers.getApplicationId} failed due to
no eligible workers.")
+ context.reply(PbRequestWorkersResponse.newBuilder()
+ .setStatus(StatusCode.SLOT_NOT_AVAILABLE.getValue)
+ .build())
+ return
+ }
+
+ if (authEnabled) {
+ pushApplicationMetaToWorkers(requestWorkers.getApplicationId,
selectedWorkers)
+ }
+ context.reply(PbRequestWorkersResponse.newBuilder()
+ .setStatus(StatusCode.SUCCESS.getValue)
+ .addAllWorkers(
+ selectedWorkers.asScala.map { worker =>
+ PbSerDeUtils.toPbWorkerInfo(worker, true, true).toBuilder
+ .setNetworkLocation(worker.networkLocation)
+ .build()
+ }.asJava)
+ .build())
+ }
+
def pushApplicationMetaToWorkers(
- requestSlots: RequestSlots,
- slots: util.Map[WorkerInfo, (util.List[PartitionLocation],
util.List[PartitionLocation])])
- : Unit = {
+ applicationId: String,
+ workers: util.Collection[WorkerInfo]): Unit = {
// Pass application registration information to the workers
val pbApplicationMeta = PbApplicationMeta.newBuilder()
- .setAppId(requestSlots.applicationId)
- .setSecret(secretRegistry.getSecretKey(requestSlots.applicationId))
+ .setAppId(applicationId)
+ .setSecret(secretRegistry.getSecretKey(applicationId))
.build()
val transportMessage =
new TransportMessage(MessageType.APPLICATION_META,
pbApplicationMeta.toByteArray)
val workerSet = workersAssignedToApp.computeIfAbsent(
- requestSlots.applicationId,
+ applicationId,
new util.function.Function[String, util.Set[WorkerInfo]] {
override def apply(key: String): util.Set[WorkerInfo] =
util.Collections.newSetFromMap(JavaUtils.newConcurrentHashMap[
WorkerInfo,
java.lang.Boolean]())
})
- slots.keySet().asScala.foreach { worker =>
+ workers.asScala.foreach { worker =>
// The app meta info is send to a Worker only if it wasn't previously
sent.
if (workerSet.add(worker)) {
sendApplicationMetaExecutor.submit(new Runnable {
diff --git
a/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSuite.scala
b/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSuite.scala
index 48ad9fe8d9..0067a69fa5 100644
---
a/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSuite.scala
+++
b/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSuite.scala
@@ -20,6 +20,8 @@ package org.apache.celeborn.service.deploy.master
import java.nio.file.Files
import java.util
+import scala.collection.JavaConverters._
+
import org.mockito.ArgumentCaptor
import org.mockito.Mockito.{mock, verify, when}
import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
@@ -27,13 +29,16 @@ import org.scalatest.funsuite.AnyFunSuite
import org.apache.celeborn.common.CelebornConf
import org.apache.celeborn.common.identity.UserIdentifier
+import org.apache.celeborn.common.meta.{DiskInfo, WorkerInfo}
import org.apache.celeborn.common.network.client.{RpcResponseCallback,
TransportClient}
-import org.apache.celeborn.common.protocol.{PbApplicationMetaRequest,
PbCheckForWorkerTimeout, PbRegisterWorker}
+import org.apache.celeborn.common.protocol.{PbApplicationMetaRequest,
PbCheckForWorkerTimeout, PbRegisterWorker, PbRequestWorkers,
PbRequestWorkersResponse}
+import org.apache.celeborn.common.protocol.StorageInfo
import
org.apache.celeborn.common.protocol.message.ControlMessages.{RequestSlots,
RequestSlotsResponse, ReviseLostShuffles}
import org.apache.celeborn.common.protocol.message.StatusCode
+import org.apache.celeborn.common.quota.ResourceConsumption
import org.apache.celeborn.common.rpc.{RpcAddress, RpcCallContext}
import org.apache.celeborn.common.rpc.netty.{NettyRpcEnv,
RemoteNettyRpcCallContext}
-import org.apache.celeborn.common.util.{CelebornExitKind, ThreadUtils}
+import org.apache.celeborn.common.util.{CelebornExitKind, PbSerDeUtils,
ThreadUtils}
class MasterSuite extends AnyFunSuite
with BeforeAndAfterAll
@@ -52,6 +57,16 @@ class MasterSuite extends AnyFunSuite
client)
}
+ private def requestWorkers(
+ master: Master,
+ request: PbRequestWorkers): PbRequestWorkersResponse = {
+ val context = mock(classOf[RpcCallContext])
+ val captor = ArgumentCaptor.forClass(classOf[Any])
+ master.handleRequestWorkers(context, request)
+ verify(context).reply(captor.capture())
+ captor.getValue.asInstanceOf[PbRequestWorkersResponse]
+ }
+
def getTmpDir(): String = {
val tmpDir = Files.createTempDirectory(null).toFile
tmpDir.deleteOnExit()
@@ -212,6 +227,183 @@ class MasterSuite extends AnyFunSuite
master.rpcEnv.shutdown()
}
+ test("handleRequestWorkers returns one failure when no workers are
available") {
+ val conf = new CelebornConf()
+ conf.set(CelebornConf.HA_ENABLED.key, "false")
+ conf.set(CelebornConf.MASTER_HTTP_PORT.key, selectRandomPort().toString)
+ val masterArgs = new MasterArguments(
+ Array("-h", "localhost", "-p", selectRandomPort().toString),
+ conf)
+ val master = new Master(conf, masterArgs)
+ val request = PbRequestWorkers.newBuilder()
+ .setApplicationId("app1")
+ .setUserIdentifier(
+ PbSerDeUtils.toPbUserIdentifier(new UserIdentifier("tenant", "user")))
+ .setMaxWorkers(10)
+ .build()
+ val response = requestWorkers(master, request)
+ assert(StatusCode.fromValue(response.getStatus) ===
StatusCode.WORKER_EXCLUDED)
+ assert(response.getWorkersCount === 0)
+ master.rpcEnv.shutdown()
+ }
+
+ test("handleRequestWorkers applies limits without allocating slots") {
+ val conf = new CelebornConf()
+ conf.set(CelebornConf.HA_ENABLED.key, "false")
+ conf.set(CelebornConf.MASTER_HTTP_PORT.key, selectRandomPort().toString)
+ conf.set(CelebornConf.MASTER_SPLIT_SLOT_ASSIGN_MAX_WORKERS.key, "2")
+ val masterArgs = new MasterArguments(
+ Array("-h", "localhost", "-p", selectRandomPort().toString),
+ conf)
+ val master = new Master(conf, masterArgs)
+ val workers = (1 to 4).map { index =>
+ val worker =
+ new WorkerInfo(s"host$index", 1000 + index, 2000 + index, 3000 +
index, 4000 + index)
+ worker.networkLocation = s"/rack-$index"
+ worker
+ }
+ master.statusSystem.availableWorkers.addAll(workers.asJava)
+ val registeredShufflesBefore =
master.statusSystem.registeredAppAndShuffles.size()
+ val request = PbRequestWorkers.newBuilder()
+ .setApplicationId("app1")
+ .setUserIdentifier(
+ PbSerDeUtils.toPbUserIdentifier(new UserIdentifier("tenant", "user")))
+ .setMaxWorkers(3)
+ .setAvailableStorageTypes(StorageInfo.MEMORY_MASK)
+ .build()
+ val response = requestWorkers(master, request)
+ val responseWorkers =
+ response.getWorkersList.asScala.map(PbSerDeUtils.fromPbWorkerInfo).toSet
+ assert(StatusCode.fromValue(response.getStatus) === StatusCode.SUCCESS)
+ assert(responseWorkers.size === 2)
+ assert(responseWorkers.subsetOf(workers.toSet))
+ val networkLocationsByHost = workers.map(worker => worker.host ->
worker.networkLocation).toMap
+ response.getWorkersList.asScala.foreach { worker =>
+ assert(worker.getNetworkLocation ===
networkLocationsByHost(worker.getHost))
+ }
+ assert(master.statusSystem.registeredAppAndShuffles.size() ===
registeredShufflesBefore)
+
+ val defaultLimitResponse = requestWorkers(master,
request.toBuilder.setMaxWorkers(0).build())
+ assert(StatusCode.fromValue(defaultLimitResponse.getStatus) ===
StatusCode.SUCCESS)
+ assert(defaultLimitResponse.getWorkersCount === 2)
+
+ val singleWorkerResponse = requestWorkers(master,
request.toBuilder.setMaxWorkers(1).build())
+ assert(StatusCode.fromValue(singleWorkerResponse.getStatus) ===
StatusCode.SUCCESS)
+ assert(singleWorkerResponse.getWorkersCount === 1)
+
+ val replicatedResponse = requestWorkers(
+ master,
+ request.toBuilder
+ .setMaxWorkers(1)
+ .setShouldReplicate(true)
+ .build())
+ assert(StatusCode.fromValue(replicatedResponse.getStatus) ===
StatusCode.SUCCESS)
+ assert(replicatedResponse.getWorkersCount === 2)
+
+ val excludedWorkers = workers.take(3)
+ val excludedResponse = requestWorkers(
+ master,
+ request.toBuilder
+ .addAllExcludedWorkerSet(
+ excludedWorkers.map(PbSerDeUtils.toPbWorkerInfo(_, true,
true)).asJava)
+ .build())
+ assert(StatusCode.fromValue(excludedResponse.getStatus) ===
StatusCode.SUCCESS)
+ assert(
+
excludedResponse.getWorkersList.asScala.map(PbSerDeUtils.fromPbWorkerInfo).toSet
===
+ workers.drop(3).toSet)
+
+ val partialReplicaResponse = requestWorkers(
+ master,
+ request.toBuilder
+ .setShouldReplicate(true)
+ .addAllExcludedWorkerSet(
+ excludedWorkers.map(PbSerDeUtils.toPbWorkerInfo(_, true,
true)).asJava)
+ .build())
+ assert(
+ StatusCode.fromValue(partialReplicaResponse.getStatus) ===
+ StatusCode.SUCCESS)
+ assert(partialReplicaResponse.getWorkersCount === 1)
+
+ val taggedResponse =
+ requestWorkers(master,
request.toBuilder.setTagsExpr("missing-tag").build())
+ assert(StatusCode.fromValue(taggedResponse.getStatus) ===
StatusCode.WORKER_EXCLUDED)
+ assert(taggedResponse.getWorkersCount === 0)
+
+ master.rpcEnv.shutdown()
+ }
+
+ test("handleRequestWorkers applies storage eligibility") {
+ val conf = new CelebornConf()
+ conf.set(CelebornConf.HA_ENABLED.key, "false")
+ conf.set(CelebornConf.MASTER_HTTP_PORT.key, selectRandomPort().toString)
+ val masterArgs = new MasterArguments(
+ Array("-h", "localhost", "-p", selectRandomPort().toString),
+ conf)
+ val master = new Master(conf, masterArgs)
+
+ def workerWithDisk(host: String, port: Int): WorkerInfo = {
+ val disk = new DiskInfo("/disk", 1024L, 0L, 0L, 0L, StorageInfo.Type.HDD)
+ val disks = new util.HashMap[String, DiskInfo]()
+ disks.put(disk.mountPoint, disk)
+ new WorkerInfo(
+ host,
+ port,
+ port + 1,
+ port + 2,
+ port + 3,
+ port + 4,
+ disks,
+ new util.HashMap[UserIdentifier, ResourceConsumption]())
+ }
+
+ val diskWorker = workerWithDisk("disk", 1000)
+ val disklessWorker = new WorkerInfo("diskless", 2000, 2001, 2002, 2003)
+ val workers = Seq(diskWorker, disklessWorker)
+ master.statusSystem.availableWorkers.addAll(workers.asJava)
+
+ val request = PbRequestWorkers.newBuilder()
+ .setApplicationId("app1")
+ .setUserIdentifier(
+ PbSerDeUtils.toPbUserIdentifier(new UserIdentifier("tenant", "user")))
+ .setMaxWorkers(10)
+ .build()
+
+ val localResponse = requestWorkers(
+ master,
+ request.toBuilder
+ .setAvailableStorageTypes(StorageInfo.LOCAL_DISK_MASK)
+ .build())
+ assert(StatusCode.fromValue(localResponse.getStatus) ===
StatusCode.SUCCESS)
+ assert(localResponse.getWorkersList.asScala.map(_.getHost).toSet ===
Set(diskWorker.host))
+
+ val noLocalDiskResponse = requestWorkers(
+ master,
+ request.toBuilder
+ .setAvailableStorageTypes(StorageInfo.LOCAL_DISK_MASK)
+ .addExcludedWorkerSet(PbSerDeUtils.toPbWorkerInfo(diskWorker, true,
true))
+ .build())
+ assert(StatusCode.fromValue(noLocalDiskResponse.getStatus) ===
StatusCode.SLOT_NOT_AVAILABLE)
+ assert(noLocalDiskResponse.getWorkersCount === 0)
+
+ val remoteResponse = requestWorkers(
+ master,
+ request.toBuilder
+ .setAvailableStorageTypes(StorageInfo.HDFS_MASK)
+ .build())
+ assert(StatusCode.fromValue(remoteResponse.getStatus) ===
StatusCode.SUCCESS)
+ assert(remoteResponse.getWorkersList.asScala.map(_.getHost).toSet ===
workers.map(_.host).toSet)
+
+ val mixedResponse = requestWorkers(
+ master,
+ request.toBuilder
+ .setAvailableStorageTypes(StorageInfo.MEMORY_MASK |
StorageInfo.LOCAL_DISK_MASK)
+ .build())
+ assert(StatusCode.fromValue(mixedResponse.getStatus) ===
StatusCode.SUCCESS)
+ assert(mixedResponse.getWorkersList.asScala.map(_.getHost).toSet ===
Set(diskWorker.host))
+
+ master.rpcEnv.shutdown()
+ }
+
test("PbApplicationMetaRequest rejects a caller requesting another
application's secret") {
val conf = new CelebornConf()
val randomMasterPort = selectRandomPort()
diff --git
a/tests/spark-it/src/test/scala/org/apache/celeborn/tests/client/ChangePartitionManagerUpdateWorkersSuite.scala
b/tests/spark-it/src/test/scala/org/apache/celeborn/tests/client/ChangePartitionManagerUpdateWorkersSuite.scala
index 6e4b081230..3ecdf8a282 100644
---
a/tests/spark-it/src/test/scala/org/apache/celeborn/tests/client/ChangePartitionManagerUpdateWorkersSuite.scala
+++
b/tests/spark-it/src/test/scala/org/apache/celeborn/tests/client/ChangePartitionManagerUpdateWorkersSuite.scala
@@ -22,6 +22,10 @@ import java.util.Collections
import scala.collection.JavaConverters.{collectionAsScalaIterableConverter,
mapAsScalaMapConverter}
+import org.scalatest.concurrent.Eventually.eventually
+import org.scalatest.concurrent.Futures.{interval, timeout}
+import org.scalatest.time.SpanSugar.convertIntToGrainOfTime
+
import org.apache.celeborn.client.{ChangePartitionManager,
ChangePartitionRequest, LifecycleManager, WithShuffleClientSuite}
import org.apache.celeborn.client.LifecycleManager.ShuffleFailedWorkers
import org.apache.celeborn.common.CelebornConf
@@ -57,7 +61,7 @@ class ChangePartitionManagerUpdateWorkersSuite extends
WithShuffleClientSuite
conf.set(CelebornConf.CLIENT_PUSH_MAX_REVIVE_TIMES.key, "3")
.set(CelebornConf.CLIENT_BATCH_HANDLE_CHANGE_PARTITION_ENABLED.key,
"false")
.set(CelebornConf.CLIENT_SHUFFLE_DYNAMIC_RESOURCE_ENABLED.key, "true")
- .set(CelebornConf.CLIENT_SHUFFLE_DYNAMIC_RESOURCE_FACTOR.key, "0.0")
+ .set(CelebornConf.CLIENT_SHUFFLE_DYNAMIC_RESOURCE_UPDATE_TIME.key, "0")
val lifecycleManager: LifecycleManager = new LifecycleManager(APP, conf)
val changePartitionManager: ChangePartitionManager =
@@ -100,7 +104,8 @@ class ChangePartitionManagerUpdateWorkersSuite extends
WithShuffleClientSuite
setUpWorkers(workerConfForAdding, 2)
assert(workerInfos.size == 3)
- 0 until 10 foreach { partitionId: Int =>
+ var partitionId = 0
+ eventually(timeout(10.seconds), interval(100.milliseconds)) {
val req = ChangePartitionRequest(
null,
shuffleId,
@@ -115,9 +120,9 @@ class ChangePartitionManagerUpdateWorkersSuite extends
WithShuffleClientSuite
shuffleId,
Array(req),
lifecycleManager.commitManager.isSegmentGranularityVisible(shuffleId))
+ partitionId += 1
+ assert(lifecycleManager.workerSnapshots(shuffleId).size() > 1)
}
- Thread.sleep(5000)
- assert(lifecycleManager.workerSnapshots(shuffleId).size() > 1)
lifecycleManager.stop()
}
@@ -131,7 +136,7 @@ class ChangePartitionManagerUpdateWorkersSuite extends
WithShuffleClientSuite
conf.set(CelebornConf.CLIENT_PUSH_MAX_REVIVE_TIMES.key, "3")
.set(CelebornConf.CLIENT_BATCH_HANDLE_CHANGE_PARTITION_ENABLED.key,
"false")
.set(CelebornConf.CLIENT_SHUFFLE_DYNAMIC_RESOURCE_ENABLED.key, "true")
- .set(CelebornConf.CLIENT_SHUFFLE_DYNAMIC_RESOURCE_FACTOR.key, "0.5")
+ .set(CelebornConf.CLIENT_SHUFFLE_DYNAMIC_RESOURCE_UPDATE_TIME.key, "0")
val lifecycleManager: LifecycleManager = new LifecycleManager(APP, conf)
val changePartitionManager: ChangePartitionManager =
@@ -202,7 +207,8 @@ class ChangePartitionManagerUpdateWorkersSuite extends
WithShuffleClientSuite
setUpWorkers(workerConfForAdding, 1)
assert(workerInfos.size == 3)
- 0 until 10 foreach { partitionId: Int =>
+ var partitionId = 0
+ eventually(timeout(10.seconds), interval(100.milliseconds)) {
val req = ChangePartitionRequest(
null,
shuffleId,
@@ -217,27 +223,27 @@ class ChangePartitionManagerUpdateWorkersSuite extends
WithShuffleClientSuite
shuffleId,
Array(req),
lifecycleManager.commitManager.isSegmentGranularityVisible(shuffleId))
+ partitionId += 1
+
+ val snapshotCandidates =
+ lifecycleManager
+ .workerSnapshots(shuffleId)
+ .asScala
+ .values
+ .map(_.workerInfo)
+ .filter(lifecycleManager.workerStatusTracker.workerAvailable)
+ assert(snapshotCandidates.size == 2)
}
-
- val snapshotCandidates =
- lifecycleManager
- .workerSnapshots(shuffleId)
- .asScala
- .values
- .map(_.workerInfo)
- .filter(lifecycleManager.workerStatusTracker.workerAvailable)
-
- assert(snapshotCandidates.size == 2)
lifecycleManager.stop()
}
- test("test changePartition with available workers and factor") {
+ test("test changePartition honors worker pool update time") {
val shuffleId = nextShuffleId
val conf = celebornConf.clone
conf.set(CelebornConf.CLIENT_PUSH_MAX_REVIVE_TIMES.key, "3")
.set(CelebornConf.CLIENT_BATCH_HANDLE_CHANGE_PARTITION_ENABLED.key,
"false")
.set(CelebornConf.CLIENT_SHUFFLE_DYNAMIC_RESOURCE_ENABLED.key, "true")
- .set(CelebornConf.CLIENT_SHUFFLE_DYNAMIC_RESOURCE_FACTOR.key, "1.0")
+ .set(CelebornConf.CLIENT_SHUFFLE_DYNAMIC_RESOURCE_UPDATE_TIME.key,
"2000s")
val lifecycleManager: LifecycleManager = new LifecycleManager(APP, conf)
val changePartitionManager: ChangePartitionManager =
@@ -278,10 +284,28 @@ class ChangePartitionManagerUpdateWorkersSuite extends
WithShuffleClientSuite
}
assert(lifecycleManager.workerSnapshots(shuffleId).size() == workerNum)
+ // The first change refreshes the pool and starts the update-time window.
+ val initialRequest = ChangePartitionRequest(
+ null,
+ shuffleId,
+ 0,
+ -1,
+ null,
+ None)
+ changePartitionManager.changePartitionRequests.computeIfAbsent(
+ shuffleId,
+ changePartitionManager.rpcContextRegisterFunc)
+ changePartitionManager.handleRequestPartitions(
+ shuffleId,
+ Array(initialRequest),
+ lifecycleManager.commitManager.isSegmentGranularityVisible(shuffleId))
+ assert(lifecycleManager.workerSnapshots(shuffleId).size() == workerNum)
+
// total workerNum is 1 + 2 = 3 now
setUpWorkers(workerConfForAdding, 2)
assert(workerInfos.size == 3)
+ // The refresh interval has not elapsed, so the newly registered workers
are not candidates.
0 until 10 foreach { partitionId: Int =>
val req = ChangePartitionRequest(
null,
diff --git
a/tests/spark-it/src/test/scala/org/apache/celeborn/tests/spark/RetryReviveTest.scala
b/tests/spark-it/src/test/scala/org/apache/celeborn/tests/spark/RetryReviveTest.scala
index ffb8e7721d..31d37cf244 100644
---
a/tests/spark-it/src/test/scala/org/apache/celeborn/tests/spark/RetryReviveTest.scala
+++
b/tests/spark-it/src/test/scala/org/apache/celeborn/tests/spark/RetryReviveTest.scala
@@ -68,7 +68,7 @@ class RetryReviveTest extends AnyFunSuite
.set(s"spark.${CelebornConf.TEST_CLIENT_RETRY_REVIVE.key}", "true")
.set(s"spark.${CelebornConf.CLIENT_PUSH_MAX_REVIVE_TIMES.key}", "3")
.set(s"spark.${CelebornConf.CLIENT_SHUFFLE_DYNAMIC_RESOURCE_ENABLED.key}",
"true")
-
.set(s"spark.${CelebornConf.CLIENT_SHUFFLE_DYNAMIC_RESOURCE_FACTOR.key}", "0")
+
.set(s"spark.${CelebornConf.CLIENT_SHUFFLE_DYNAMIC_RESOURCE_UPDATE_TIME.key}",
"0")
.set(s"spark.${CelebornConf.MASTER_SLOT_ASSIGN_EXTRA_SLOTS.key}", "0")
.setAppName("celeborn-demo").setMaster("local[2]")
val ss = SparkSession.builder()