Skip to content

Commit aa97292

Browse files
FMXzaynt4606
authored andcommitted
[CELEBORN-1601] Support revise lost shuffles
### What changes were proposed in this pull request? To support revising lost shuffle IDs in a long-running job such as flink batch jobs. ### Why are the changes needed? 1. To support revise lost shuffles. 2. To add an HTTP endpoint to revise lost shuffles manually. ### Does this PR introduce _any_ user-facing change? NO. ### How was this patch tested? Cluster tests. Closes apache#2746 from FMX/b1600. Lead-authored-by: mingji <fengmingxiao.fmx@alibaba-inc.com> Co-authored-by: Ethan Feng <fengmingxiao.fmx@alibaba-inc.com> Signed-off-by: SteNicholas <programgeek@163.com>
1 parent 8f70daf commit aa97292

32 files changed

Lines changed: 652 additions & 203 deletions

File tree

cli/src/main/scala/org/apache/celeborn/cli/common/CommonOptions.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,11 @@ class CommonOptions {
7171
paramLabel = "username",
7272
description = Array("The username of the TENANT_USER level."))
7373
private[cli] var configName: String = _
74+
75+
@Option(
76+
names = Array("--apps"),
77+
paramLabel = "appId",
78+
description = Array("The application Id list seperated by comma."))
79+
private[cli] var apps: String = _
80+
7481
}

cli/src/main/scala/org/apache/celeborn/cli/master/MasterOptions.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,14 @@ final class MasterOptions {
110110
names = Array("--remove-workers-unavailable-info"),
111111
description = Array("Remove the workers unavailable info from the master."))
112112
private[master] var removeWorkersUnavailableInfo: Boolean = _
113+
114+
@Option(
115+
names = Array("--revise-lost-shuffles"),
116+
description = Array("Revise lost shuffles or remove shuffles for an application."))
117+
private[master] var reviseLostShuffles: Boolean = _
118+
119+
@Option(
120+
names = Array("--delete-apps"),
121+
description = Array("Delete resource of an application."))
122+
private[master] var deleteApps: Boolean = _
113123
}

cli/src/main/scala/org/apache/celeborn/cli/master/MasterSubcommand.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ trait MasterSubcommand extends CliLogging {
3737
@ArgGroup(exclusive = true, multiplicity = "1")
3838
private[master] var masterOptions: MasterOptions = _
3939

40+
@ArgGroup(exclusive = false)
41+
private[master] var reviseLostShuffleOptions: ReviseLostShuffleOptions = _
42+
4043
@Mixin
4144
private[master] var commonOptions: CommonOptions = _
4245

@@ -110,4 +113,8 @@ trait MasterSubcommand extends CliLogging {
110113

111114
private[master] def runShowThreadDump: ThreadStackResponse
112115

116+
private[master] def reviseLostShuffles: HandleResponse
117+
118+
private[master] def deleteApps: HandleResponse
119+
113120
}

cli/src/main/scala/org/apache/celeborn/cli/master/MasterSubcommandImpl.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ class MasterSubcommandImpl extends Runnable with MasterSubcommand {
5151
if (masterOptions.showContainerInfo) log(runShowContainerInfo)
5252
if (masterOptions.showDynamicConf) log(runShowDynamicConf)
5353
if (masterOptions.showThreadDump) log(runShowThreadDump)
54+
if (masterOptions.reviseLostShuffles) log(reviseLostShuffles)
55+
if (masterOptions.deleteApps) log(deleteApps)
5456
if (masterOptions.addClusterAlias != null && masterOptions.addClusterAlias.nonEmpty)
5557
runAddClusterAlias
5658
if (masterOptions.removeClusterAlias != null && masterOptions.removeClusterAlias.nonEmpty)
@@ -220,4 +222,20 @@ class MasterSubcommandImpl extends Runnable with MasterSubcommand {
220222
}
221223

222224
private[master] def runShowContainerInfo: ContainerInfo = defaultApi.getContainerInfo
225+
226+
override private[master] def reviseLostShuffles: HandleResponse = {
227+
val app = commonOptions.apps
228+
if (app.contains(",")) {
229+
throw new ParameterException(
230+
spec.commandLine(),
231+
"Only one application id can be provided for this command.")
232+
}
233+
val shuffleIds = reviseLostShuffleOptions.shuffleIds
234+
applicationApi.reviseLostShuffles(app, shuffleIds)
235+
}
236+
237+
override private[master] def deleteApps: HandleResponse = {
238+
val apps = commonOptions.apps
239+
applicationApi.deleteApps(apps)
240+
}
223241
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.celeborn.cli.master
19+
20+
import picocli.CommandLine.Option
21+
22+
final class ReviseLostShuffleOptions {
23+
24+
@Option(
25+
names = Array("--shuffleIds"),
26+
description = Array("The shuffle ids to manipulate."))
27+
private[master] var shuffleIds: String = _
28+
29+
}

cli/src/test/scala/org/apache/celeborn/cli/TestCelebornCliCommands.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,32 @@ class TestCelebornCliCommands extends CelebornFunSuite with MiniClusterFeature {
247247
captureOutputAndValidateResponse(args, "success: true")
248248
}
249249

250+
test("master --delete-apps case1") {
251+
val args = prepareMasterArgs() ++ Array(
252+
"--delete-apps",
253+
"--apps",
254+
"app1")
255+
captureOutputAndValidateResponse(args, "success: true")
256+
}
257+
258+
test("master --delete-apps case2") {
259+
val args = prepareMasterArgs() ++ Array(
260+
"--delete-apps",
261+
"--apps",
262+
"app1,app2")
263+
captureOutputAndValidateResponse(args, "success: true")
264+
}
265+
266+
test("master --revise-lost-shuffles case1") {
267+
val args = prepareMasterArgs() ++ Array(
268+
"--revise-lost-shuffles",
269+
"--apps",
270+
"app1",
271+
"--shuffleIds",
272+
"1,2,3,4,5,6")
273+
captureOutputAndValidateResponse(args, "success: true")
274+
}
275+
250276
private def prepareMasterArgs(): Array[String] = {
251277
Array(
252278
"master",

client/src/main/scala/org/apache/celeborn/client/ApplicationHeartbeater.scala

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@
1717

1818
package org.apache.celeborn.client
1919

20-
import java.util.concurrent.{ScheduledFuture, TimeUnit}
20+
import java.util
21+
import java.util.concurrent.{ConcurrentHashMap, ScheduledFuture, TimeUnit}
22+
import java.util.function.Consumer
2123

2224
import scala.collection.JavaConverters._
2325

26+
import org.apache.commons.lang3.StringUtils
27+
2428
import org.apache.celeborn.common.CelebornConf
2529
import org.apache.celeborn.common.client.MasterClient
2630
import org.apache.celeborn.common.internal.Logging
27-
import org.apache.celeborn.common.protocol.message.ControlMessages.{ApplicationLost, ApplicationLostResponse, HeartbeatFromApplication, HeartbeatFromApplicationResponse, ZERO_UUID}
31+
import org.apache.celeborn.common.protocol.PbReviseLostShufflesResponse
32+
import org.apache.celeborn.common.protocol.message.ControlMessages.{ApplicationLost, ApplicationLostResponse, HeartbeatFromApplication, HeartbeatFromApplicationResponse, ReviseLostShuffles, ZERO_UUID}
2833
import org.apache.celeborn.common.protocol.message.StatusCode
2934
import org.apache.celeborn.common.util.{ThreadUtils, Utils}
3035

@@ -33,9 +38,11 @@ class ApplicationHeartbeater(
3338
conf: CelebornConf,
3439
masterClient: MasterClient,
3540
shuffleMetrics: () => (Long, Long),
36-
workerStatusTracker: WorkerStatusTracker) extends Logging {
41+
workerStatusTracker: WorkerStatusTracker,
42+
registeredShuffles: ConcurrentHashMap.KeySetView[Int, java.lang.Boolean]) extends Logging {
3743

3844
private var stopped = false
45+
private val reviseLostShuffles = conf.reviseLostShufflesEnabled
3946

4047
// Use independent app heartbeat threads to avoid being blocked by other operations.
4148
private val appHeartbeatIntervalMs = conf.appHeartbeatIntervalMs
@@ -70,6 +77,30 @@ class ApplicationHeartbeater(
7077
if (response.statusCode == StatusCode.SUCCESS) {
7178
logDebug("Successfully send app heartbeat.")
7279
workerStatusTracker.handleHeartbeatResponse(response)
80+
// revise shuffle id if there are lost shuffles
81+
if (reviseLostShuffles) {
82+
val masterRecordedShuffleIds = response.registeredShuffles
83+
val localOnlyShuffles = new util.ArrayList[Integer]()
84+
registeredShuffles.forEach(new Consumer[Int] {
85+
override def accept(key: Int): Unit = {
86+
localOnlyShuffles.add(key)
87+
}
88+
})
89+
localOnlyShuffles.removeAll(masterRecordedShuffleIds)
90+
if (!localOnlyShuffles.isEmpty) {
91+
logWarning(
92+
s"There are lost shuffle found ${StringUtils.join(localOnlyShuffles, ",")}, revise lost shuffles.")
93+
val reviseLostShufflesResponse = masterClient.askSync(
94+
ReviseLostShuffles.apply(appId, localOnlyShuffles, MasterClient.genRequestId()),
95+
classOf[PbReviseLostShufflesResponse])
96+
if (!reviseLostShufflesResponse.getSuccess) {
97+
logWarning(
98+
s"Revise lost shuffles failed. Error message :${reviseLostShufflesResponse.getMessage}")
99+
} else {
100+
logInfo("Revise lost shuffles succeed.")
101+
}
102+
}
103+
}
73104
}
74105
} catch {
75106
case it: InterruptedException =>

client/src/main/scala/org/apache/celeborn/client/LifecycleManager.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,8 @@ class LifecycleManager(val appUniqueId: String, val conf: CelebornConf) extends
210210
conf,
211211
masterClient,
212212
() => commitManager.commitMetrics(),
213-
workerStatusTracker)
213+
workerStatusTracker,
214+
registeredShuffle)
214215
private val changePartitionManager = new ChangePartitionManager(conf, this)
215216
private val releasePartitionManager = new ReleasePartitionManager(conf, this)
216217

client/src/test/scala/org/apache/celeborn/client/WorkerStatusTrackerSuite.scala

Lines changed: 11 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -23,97 +23,24 @@ import org.junit.Assert
2323

2424
import org.apache.celeborn.CelebornFunSuite
2525
import org.apache.celeborn.common.CelebornConf
26-
import org.apache.celeborn.common.CelebornConf.{APPLICATION_HEARTBEAT_WITH_AVAILABLE_WORKERS_ENABLE, CLIENT_EXCLUDED_WORKER_EXPIRE_TIMEOUT}
26+
import org.apache.celeborn.common.CelebornConf.CLIENT_EXCLUDED_WORKER_EXPIRE_TIMEOUT
2727
import org.apache.celeborn.common.meta.WorkerInfo
2828
import org.apache.celeborn.common.protocol.message.ControlMessages.HeartbeatFromApplicationResponse
2929
import org.apache.celeborn.common.protocol.message.StatusCode
3030

3131
class WorkerStatusTrackerSuite extends CelebornFunSuite {
32-
test("handleHeartbeatResponse without availableWorkers") {
33-
val celebornConf = new CelebornConf()
34-
celebornConf.set(CLIENT_EXCLUDED_WORKER_EXPIRE_TIMEOUT, 2000L)
35-
celebornConf.set(APPLICATION_HEARTBEAT_WITH_AVAILABLE_WORKERS_ENABLE, false)
36-
val statusTracker = new WorkerStatusTracker(celebornConf, null)
37-
38-
val registerTime = System.currentTimeMillis()
39-
statusTracker.excludedWorkers.put(mock("host1"), (StatusCode.WORKER_UNKNOWN, registerTime))
40-
statusTracker.excludedWorkers.put(mock("host2"), (StatusCode.WORKER_SHUTDOWN, registerTime))
41-
42-
// test reserve (only statusCode list in handleHeartbeatResponse)
43-
val empty = buildResponse(Array.empty, Array.empty, Array.empty, Array.empty)
44-
statusTracker.handleHeartbeatResponse(empty)
45-
46-
// only reserve host1
47-
Assert.assertEquals(
48-
statusTracker.excludedWorkers.get(mock("host1")),
49-
(StatusCode.WORKER_UNKNOWN, registerTime))
50-
Assert.assertFalse(statusTracker.excludedWorkers.containsKey(mock("host2")))
51-
52-
// add shutdown/excluded worker
53-
val response1 =
54-
buildResponse(Array("host0"), Array("host1", "host3"), Array("host4"), Array.empty)
55-
statusTracker.handleHeartbeatResponse(response1)
56-
57-
// test keep Unknown register time
58-
Assert.assertEquals(
59-
statusTracker.excludedWorkers.get(mock("host1")),
60-
(StatusCode.WORKER_UNKNOWN, registerTime))
61-
62-
// test new added shutdown/excluded workers
63-
Assert.assertTrue(statusTracker.excludedWorkers.containsKey(mock("host0")))
64-
Assert.assertTrue(statusTracker.excludedWorkers.containsKey(mock("host3")))
65-
Assert.assertTrue(!statusTracker.excludedWorkers.containsKey(mock("host4")))
66-
Assert.assertTrue(statusTracker.shuttingWorkers.contains(mock("host4")))
67-
68-
// test re heartbeat with shutdown workers
69-
val response2 = buildResponse(Array.empty, Array.empty, Array("host4"), Array.empty)
70-
statusTracker.handleHeartbeatResponse(response2)
71-
Assert.assertTrue(!statusTracker.excludedWorkers.containsKey(mock("host4")))
72-
Assert.assertTrue(statusTracker.shuttingWorkers.contains(mock("host4")))
73-
74-
// test remove
75-
val workers = new util.HashSet[WorkerInfo]
76-
workers.add(mock("host3"))
77-
statusTracker.removeFromExcludedWorkers(workers)
78-
Assert.assertFalse(statusTracker.excludedWorkers.containsKey(mock("host3")))
79-
80-
// test register time elapsed
81-
Thread.sleep(3000)
82-
val response3 = buildResponse(Array.empty, Array("host5", "host6"), Array.empty, Array.empty)
83-
statusTracker.handleHeartbeatResponse(response3)
84-
Assert.assertEquals(statusTracker.excludedWorkers.size(), 2)
85-
Assert.assertFalse(statusTracker.excludedWorkers.containsKey(mock("host1")))
86-
87-
// test available workers
88-
Assert.assertEquals(statusTracker.availableWorkers.size(), 0)
89-
val response4 = buildResponse(
90-
Array.empty,
91-
Array.empty,
92-
Array.empty,
93-
Array("host5", "host6", "host7", "host8"))
94-
statusTracker.handleHeartbeatResponse(response4)
95-
96-
// availableWorkers wont update through heartbeat
97-
// when APPLICATION_HEARTBEAT_WITH_AVAILABLE_WORKERS_ENABLE set to false
98-
Assert.assertEquals(statusTracker.availableWorkers.size(), 0)
99-
// available workers won't overwrite excluded workers
100-
Assert.assertEquals(statusTracker.excludedWorkers.size(), 2)
101-
Assert.assertTrue(statusTracker.excludedWorkers.containsKey(mock("host5")))
102-
Assert.assertTrue(statusTracker.excludedWorkers.containsKey(mock("host6")))
103-
}
10432

105-
test("handleHeartbeatResponse with availableWorkers") {
33+
test("handleHeartbeatResponse") {
10634
val celebornConf = new CelebornConf()
10735
celebornConf.set(CLIENT_EXCLUDED_WORKER_EXPIRE_TIMEOUT, 2000L)
108-
celebornConf.set(APPLICATION_HEARTBEAT_WITH_AVAILABLE_WORKERS_ENABLE, true)
10936
val statusTracker = new WorkerStatusTracker(celebornConf, null)
11037

11138
val registerTime = System.currentTimeMillis()
11239
statusTracker.excludedWorkers.put(mock("host1"), (StatusCode.WORKER_UNKNOWN, registerTime))
11340
statusTracker.excludedWorkers.put(mock("host2"), (StatusCode.WORKER_SHUTDOWN, registerTime))
11441

11542
// test reserve (only statusCode list in handleHeartbeatResponse)
116-
val empty = buildResponse(Array.empty, Array.empty, Array.empty, Array.empty)
43+
val empty = buildResponse(Array.empty, Array.empty, Array.empty)
11744
statusTracker.handleHeartbeatResponse(empty)
11845

11946
// only reserve host1
@@ -123,23 +50,23 @@ class WorkerStatusTrackerSuite extends CelebornFunSuite {
12350
Assert.assertFalse(statusTracker.excludedWorkers.containsKey(mock("host2")))
12451

12552
// add shutdown/excluded worker
126-
val response1 =
127-
buildResponse(Array("host0"), Array("host1", "host3"), Array("host4"), Array.empty)
53+
val response1 = buildResponse(Array("host0"), Array("host1", "host3"), Array("host4"))
12854
statusTracker.handleHeartbeatResponse(response1)
12955

13056
// test keep Unknown register time
13157
Assert.assertEquals(
13258
statusTracker.excludedWorkers.get(mock("host1")),
13359
(StatusCode.WORKER_UNKNOWN, registerTime))
60+
13461
// test new added workers
13562
Assert.assertTrue(statusTracker.excludedWorkers.containsKey(mock("host0")))
13663
Assert.assertTrue(statusTracker.excludedWorkers.containsKey(mock("host3")))
13764
Assert.assertTrue(!statusTracker.excludedWorkers.containsKey(mock("host4")))
13865
Assert.assertTrue(statusTracker.shuttingWorkers.contains(mock("host4")))
13966

14067
// test re heartbeat with shutdown workers
141-
val response2 = buildResponse(Array.empty, Array.empty, Array("host4"), Array.empty)
142-
statusTracker.handleHeartbeatResponse(response2)
68+
val response3 = buildResponse(Array.empty, Array.empty, Array("host4"))
69+
statusTracker.handleHeartbeatResponse(response3)
14370
Assert.assertTrue(!statusTracker.excludedWorkers.containsKey(mock("host4")))
14471
Assert.assertTrue(statusTracker.shuttingWorkers.contains(mock("host4")))
14572

@@ -151,49 +78,25 @@ class WorkerStatusTrackerSuite extends CelebornFunSuite {
15178

15279
// test register time elapsed
15380
Thread.sleep(3000)
154-
val response3 = buildResponse(Array.empty, Array("host5", "host6"), Array.empty, Array.empty)
155-
statusTracker.handleHeartbeatResponse(response3)
81+
val response2 = buildResponse(Array.empty, Array("host5", "host6"), Array.empty)
82+
statusTracker.handleHeartbeatResponse(response2)
15683
Assert.assertEquals(statusTracker.excludedWorkers.size(), 2)
15784
Assert.assertFalse(statusTracker.excludedWorkers.containsKey(mock("host1")))
158-
159-
// test available workers
160-
Assert.assertEquals(statusTracker.availableWorkers.size(), 0)
161-
val response4 = buildResponse(
162-
Array.empty,
163-
Array.empty,
164-
Array.empty,
165-
Array("host5", "host6", "host7", "host8"))
166-
statusTracker.handleHeartbeatResponse(response4)
167-
Assert.assertEquals(statusTracker.availableWorkers.size(), 2)
168-
// available workers won't overwrite excluded workers
169-
Assert.assertEquals(statusTracker.excludedWorkers.size(), 2)
170-
Assert.assertTrue(statusTracker.excludedWorkers.containsKey(mock("host5")))
171-
Assert.assertTrue(statusTracker.excludedWorkers.containsKey(mock("host6")))
172-
173-
// test re heartbeat with available workers
174-
val response5 = buildResponse(Array.empty, Array.empty, Array.empty, Array("host8", "host9"))
175-
statusTracker.handleHeartbeatResponse(response5)
176-
Assert.assertEquals(statusTracker.availableWorkers.size(), 2)
177-
Assert.assertFalse(statusTracker.availableWorkers.contains(mock("host7")))
178-
Assert.assertTrue(statusTracker.availableWorkers.contains(mock("host8")))
179-
Assert.assertTrue(statusTracker.availableWorkers.contains(mock("host9")))
18085
}
18186

18287
private def buildResponse(
18388
excludedWorkerHosts: Array[String],
18489
unknownWorkerHosts: Array[String],
185-
shuttingWorkerHosts: Array[String],
186-
availableWorkerHosts: Array[String]): HeartbeatFromApplicationResponse = {
90+
shuttingWorkerHosts: Array[String]): HeartbeatFromApplicationResponse = {
18791
val excludedWorkers = mockWorkers(excludedWorkerHosts)
18892
val unknownWorkers = mockWorkers(unknownWorkerHosts)
18993
val shuttingWorkers = mockWorkers(shuttingWorkerHosts)
190-
val availableWorkers = mockWorkers(availableWorkerHosts)
19194
HeartbeatFromApplicationResponse(
19295
StatusCode.SUCCESS,
19396
excludedWorkers,
19497
unknownWorkers,
19598
shuttingWorkers,
196-
availableWorkers)
99+
new util.ArrayList[Integer]())
197100
}
198101

199102
private def mockWorkers(workerHosts: Array[String]): util.ArrayList[WorkerInfo] = {

0 commit comments

Comments
 (0)