Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
310263c
[SPARK-24248][K8S] Use the Kubernetes API to populate an event queue …
mccheah May 18, 2018
60990f1
Fix build
mccheah May 18, 2018
f3bb80a
Dependencies
mccheah May 18, 2018
3343ba6
Adding some logging.
mccheah May 18, 2018
30b7f17
Specifically initialize things with null. More logs.
mccheah May 18, 2018
522b079
Fix scalastyle
mccheah May 18, 2018
600e25f
Actually create the pods
mccheah May 18, 2018
931529a
Fix build
mccheah May 18, 2018
9e5abfb
Request only one pod at a time. Use logNonFatalError to log exceptions.
mccheah May 21, 2018
2156a20
Initial tests
mccheah May 21, 2018
aabc187
Don't use GNU Trove
mccheah May 21, 2018
ee0d196
Add another test.
mccheah May 22, 2018
c2b9733
Fix dependencies
mccheah May 22, 2018
caffe23
More tests
mccheah May 22, 2018
ca3fdb3
Publish pod updates to the pod allocator and lifecycle manager separa…
mccheah May 23, 2018
79ebaf3
Ensure we make a request round immediately
mccheah May 24, 2018
fadbe9f
Remove a comment
mccheah May 24, 2018
4f58393
Fix compilation
mccheah May 24, 2018
2a2374c
Don't use tabs
mccheah May 24, 2018
5850439
Use PublishSubject instead of a LinkedBlockingQueue at all
mccheah May 24, 2018
d4cf40f
Add tests. Adjust observable concurrency.
mccheah May 24, 2018
c398ebb
Add more tests.
mccheah May 25, 2018
45a02de
Minor style
mccheah May 25, 2018
a8a3539
Minor style
mccheah May 25, 2018
4a49677
Address comments.
mccheah May 25, 2018
57ea5dd
Address more comments
mccheah May 25, 2018
b30ed39
Address comments.
mccheah May 29, 2018
5b9c00f
Close k8s client
mccheah May 30, 2018
260d82c
Addressed comments.
mccheah May 30, 2018
bd03451
Addressed comments.
mccheah Jun 1, 2018
f294dca
Small style
mccheah Jun 4, 2018
7bf49ba
More small style tweaks
mccheah Jun 4, 2018
b5c0fbf
Remove unnecessary parens
mccheah Jun 4, 2018
c4b87d8
Various style fixes
mccheah Jun 4, 2018
8615c06
Process cluster snapshots instead of deltas.
mccheah Jun 8, 2018
0a205f6
Merge remote-tracking branch 'apache/master' into event-queue-driven-…
mccheah Jun 8, 2018
3b85ab5
Remove hanging comment
mccheah Jun 8, 2018
edc982b
Remove incorrect comment
mccheah Jun 8, 2018
e077c7e
Fix log message
mccheah Jun 8, 2018
a97fc5d
Whitespace
mccheah Jun 8, 2018
bd7b0d3
Clear all executors from the snapshot from newlyCreatedExecutors
mccheah Jun 8, 2018
e9d7c8f
Add a TODO for dynamic allocation
mccheah Jun 8, 2018
0fac4d5
Address comments. Pass whole buffer of snapshots.
mccheah Jun 8, 2018
e42dd4f
Merge remote-tracking branch 'apache/master' into event-queue-driven-…
mccheah Jun 8, 2018
03b1064
Merge remote-tracking branch 'apache/master' into event-queue-driven-…
mccheah Jun 8, 2018
c1b8431
Rename classes
mccheah Jun 8, 2018
108181d
Don't use RxJava
mccheah Jun 12, 2018
9e0b758
Update manifest
mccheah Jun 12, 2018
8b0a211
Remove extra parens
mccheah Jun 12, 2018
1a99dce
Address comments. Make subscriber thread pool instead of single thread
mccheah Jun 14, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ The text of each license is also included at licenses/LICENSE-[project].txt.

(BSD 3 Clause) netlib core (com.github.fommil.netlib:core:1.1.2 - https://github.com/fommil/netlib-java/core)
(BSD 3 Clause) JPMML-Model (org.jpmml:pmml-model:1.2.7 - https://github.com/jpmml/jpmml-model)
(BSD 3 Clause) jmock (org.jmock:jmock-junit4:2.8.4 - http://jmock.org/)
(BSD License) AntLR Parser Generator (antlr:antlr:2.7.7 - http://www.antlr.org/)
(BSD License) ANTLR 4.5.2-1 (org.antlr:antlr4:4.5.2-1 - http://wwww.antlr.org/)
(BSD licence) ANTLR ST4 4.0.4 (org.antlr:ST4:4.0.4 - http://www.stringtemplate.org)
Expand Down
31 changes: 28 additions & 3 deletions core/src/main/scala/org/apache/spark/util/ThreadUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ package org.apache.spark.util

import java.util.concurrent._

import com.google.common.util.concurrent.{MoreExecutors, ThreadFactoryBuilder}
import scala.concurrent.{Awaitable, ExecutionContext, ExecutionContextExecutor}
import scala.concurrent.duration.Duration
import scala.concurrent.duration.{Duration, FiniteDuration}
import scala.concurrent.forkjoin.{ForkJoinPool => SForkJoinPool, ForkJoinWorkerThread => SForkJoinWorkerThread}
import scala.util.control.NonFatal

import com.google.common.util.concurrent.{MoreExecutors, ThreadFactoryBuilder}

import org.apache.spark.SparkException

private[spark] object ThreadUtils {
Expand Down Expand Up @@ -103,6 +102,22 @@ private[spark] object ThreadUtils {
executor
}

/**
* Wrapper over ScheduledThreadPoolExecutor.
*/
def newDaemonThreadPoolScheduledExecutor(threadNamePrefix: String, numThreads: Int)
: ScheduledExecutorService = {
val threadFactory = new ThreadFactoryBuilder()
.setDaemon(true)
.setNameFormat(s"$threadNamePrefix-%d")
.build()
val executor = new ScheduledThreadPoolExecutor(numThreads, threadFactory)
// By default, a cancelled task is not automatically removed from the work queue until its delay
// elapses. We have to enable it manually.
executor.setRemoveOnCancelPolicy(true)
executor
}

/**
* Run a piece of code in a new thread and return the result. Exception in the new thread is
* thrown in the caller thread with an adjusted stack trace that removes references to this
Expand Down Expand Up @@ -229,4 +244,14 @@ private[spark] object ThreadUtils {
}
}
// scalastyle:on awaitready

def shutdown(
executor: ExecutorService,
gracePeriod: Duration = FiniteDuration(30, TimeUnit.SECONDS)): Unit = {
executor.shutdown()
executor.awaitTermination(gracePeriod.toMillis, TimeUnit.MILLISECONDS)
if (!executor.isShutdown) {
executor.shutdownNow()
}
}
}
28 changes: 28 additions & 0 deletions licenses/LICENSE-jmock.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Copyright (c) 2000-2017, jMock.org
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer. Redistributions
in binary form must reproduce the above copyright notice, this list of
conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.

Neither the name of jMock nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,12 @@
<version>1.10.19</version>
<scope>test</scope>
</dependency>
<dependency>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why adding this to the top level pom?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always add to the top level and then in the lower level poms, we reference the dependent modules without listing their versions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm a bit concerned adding rxjava to the top level pom and to dev/deps/spark-deps-hadoop-*
can it be just a <arrow.version>0.8.0</arrow.version> thing and not a dependency?

it might possibly conflict with calling Spark from the Reactive Stream stack? @skonto what do you think?

Copy link
Member

@felixcheung felixcheung May 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also added dependency should have its LICENSE added under /license

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm a bit concerned adding rxjava to the top level pom and to dev/deps/spark-deps-hadoop-*
can it be just a <arrow.version>0.8.0</arrow.version> thing and not a dependency?

Unsure what you mean here - we're using rxjava itself specifically to do the event handling in this PR. See https://github.com/apache/spark/pull/21366/files#diff-ae4cd884779fb4c3db58958ab984db59R40. If we wanted an alternative approach we can build something from first principles (executor service / manual linked blocking queues) but I like the elegance that rx-java buys us here. The code we'd save building ourselves seems worthwhile.

Copy link
Contributor

@skonto skonto Jun 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does Akka streams without JDK 9 depend on ReactiveX?

It depends on reactive streams library so you dont need to bring rx-Java in. @ktoso correct me if I am wrong.

In this regard we are no different from the other custom controllers in the Kubernetes ecosystem which have to handle managing large number of pods.

Have an example of a specific controller to get a better understanding?

Copy link

@ktoso ktoso Jun 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Akka Streams does not depend on Rx of course, they both alternative implementations of Reactive Streams ( http://reactive-streams.org/ ) which have been included in JDK9 as java.util.concurrent.Flow.* and Akka also implements those, but does not require JDK9; you can use JDK8 + RS and if you use JDK9 you could use the JDK's types but it's not required. Both Akka and Rx implement the respective interfaces in RS / JDK, so can inter-op thanks to that (see the RS site for details).

Anything else I should clarify or review here? For inter-op purposes it would be good to not expose on a specific implementation but expose the reactive-streams types (org.reactivestreams.Publisher etc), but that only matters if the types are exposed. As for including dependencies in core Spark -- I would expect this to carry quite a bit of implications though don't know Spark's rules about it (ofc less dependencies == better for users, since less chances to version-clash with libraries they'd use)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These types are not exposed - they're only implementation details in the Kubernetes module. Furthermore the RxJava dependency will be in the Spark distribution but is not a dependency pulled in by spark-core.

It sounds like there is some contention with the extra dependency though, so should we be considering implementing our own mechanisms from the ground up? I think the bottom line question is: can spark-kubernetes, NOT spark-core, pull in RxJava?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up just removing reactive programming entirely - the buffering is implemented manually. Please take a look.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanx @ktoso!

<groupId>org.jmock</groupId>
<artifactId>jmock-junit4</artifactId>
<scope>test</scope>
<version>2.8.4</version>
</dependency>
<dependency>
<groupId>org.scalacheck</groupId>
<artifactId>scalacheck_${scala.binary.version}</artifactId>
Expand Down
12 changes: 9 additions & 3 deletions resource-managers/kubernetes/core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,22 @@
</dependency>
<!-- End of shaded deps. -->

<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.8.1</version>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.8.1</version>
<groupId>org.jmock</groupId>
<artifactId>jmock-junit4</artifactId>
<scope>test</scope>
</dependency>

</dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,24 @@ private[spark] object Config extends Logging {
.checkValue(interval => interval > 0, s"Logging interval must be a positive time value.")
.createWithDefaultString("1s")

val KUBERNETES_EXECUTOR_API_POLLING_INTERVAL =
ConfigBuilder("spark.kubernetes.executor.apiPollingInterval")
.doc("Interval between polls against the Kubernetes API server to inspect the " +
"state of executors.")
.timeConf(TimeUnit.MILLISECONDS)
.checkValue(interval => interval > 0, s"API server polling interval must be a" +
" positive time value.")
.createWithDefaultString("30s")

val KUBERNETES_EXECUTOR_EVENT_PROCESSING_INTERVAL =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this option is hard to reason about and relies on understanding an implementation detail (the event queue). Why not just pick a default and leave it at that? What scenario do we see for the user to try and choose this value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to leave this configurable but not to document it. It would give users an escape hatch if for whatever reason they do need to adjust the timing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mccheah because this is not internal.... shouldn't we include this in docs/ I don't see why we aren't documenting this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have been marked with internal(), that was an oversight. Don't think it's entirely necessary.

ConfigBuilder("spark.kubernetes.executor.eventProcessingInterval")
.doc("Interval between successive inspection of executor events sent from the" +
" Kubernetes API.")
.timeConf(TimeUnit.MILLISECONDS)
.checkValue(interval => interval > 0, s"Event processing interval must be a positive" +
" time value.")
.createWithDefaultString("1s")

val MEMORY_OVERHEAD_FACTOR =
ConfigBuilder("spark.kubernetes.memoryOverheadFactor")
.doc("This sets the Memory Overhead Factor that will allocate memory to non-JVM jobs " +
Expand All @@ -193,7 +211,6 @@ private[spark] object Config extends Logging {
"Ensure that major Python version is either Python2 or Python3")
.createWithDefault("2")


val KUBERNETES_AUTH_SUBMISSION_CONF_PREFIX =
"spark.kubernetes.authenticate.submission"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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.spark.scheduler.cluster.k8s

import io.fabric8.kubernetes.api.model.Pod

sealed trait ExecutorPodState {
def pod: Pod
}

case class PodRunning(pod: Pod) extends ExecutorPodState

case class PodPending(pod: Pod) extends ExecutorPodState

sealed trait FinalPodState extends ExecutorPodState

case class PodSucceeded(pod: Pod) extends FinalPodState

case class PodFailed(pod: Pod) extends FinalPodState

case class PodDeleted(pod: Pod) extends FinalPodState

case class PodUnknown(pod: Pod) extends ExecutorPodState
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* 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.spark.scheduler.cluster.k8s

import java.util.concurrent.atomic.{AtomicInteger, AtomicLong}

import io.fabric8.kubernetes.api.model.PodBuilder
import io.fabric8.kubernetes.client.KubernetesClient
import scala.collection.mutable

import org.apache.spark.{SparkConf, SparkException}
import org.apache.spark.deploy.k8s.Config._
import org.apache.spark.deploy.k8s.Constants._
import org.apache.spark.deploy.k8s.KubernetesConf
import org.apache.spark.internal.Logging
import org.apache.spark.util.{Clock, Utils}

private[spark] class ExecutorPodsAllocator(
conf: SparkConf,
executorBuilder: KubernetesExecutorBuilder,
kubernetesClient: KubernetesClient,
snapshotsStore: ExecutorPodsSnapshotsStore,
clock: Clock) extends Logging {

private val EXECUTOR_ID_COUNTER = new AtomicLong(0L)

private val totalExpectedExecutors = new AtomicInteger(0)

private val podAllocationSize = conf.get(KUBERNETES_ALLOCATION_BATCH_SIZE)

private val podAllocationDelay = conf.get(KUBERNETES_ALLOCATION_BATCH_DELAY)

private val podCreationTimeout = math.max(podAllocationDelay * 5, 60000)

private val kubernetesDriverPodName = conf
.get(KUBERNETES_DRIVER_POD_NAME)
.getOrElse(throw new SparkException("Must specify the driver pod name"))

private val driverPod = kubernetesClient.pods()
.withName(kubernetesDriverPodName)
.get()

// Executor IDs that have been requested from Kubernetes but have not been detected in any
// snapshot yet. Mapped to the timestamp when they were created.
private val newlyCreatedExecutors = mutable.Map.empty[Long, Long]

def start(applicationId: String): Unit = {
snapshotsStore.addSubscriber(podAllocationDelay) {
onNewSnapshots(applicationId, _)
}
}

def setTotalExpectedExecutors(total: Int): Unit = totalExpectedExecutors.set(total)

private def onNewSnapshots(applicationId: String, snapshots: Seq[ExecutorPodsSnapshot]): Unit = {
newlyCreatedExecutors --= snapshots.flatMap(_.executorPods.keys)
// For all executors we've created against the API but have not seen in a snapshot
// yet - check the current time. If the current time has exceeded some threshold,
// assume that the pod was either never created (the API server never properly
// handled the creation request), or the API server created the pod but we missed
// both the creation and deletion events. In either case, delete the missing pod
// if possible, and mark such a pod to be rescheduled below.
newlyCreatedExecutors.foreach { case (execId, timeCreated) =>
val currentTime = clock.getTimeMillis()
if (currentTime - timeCreated > podCreationTimeout) {
logWarning(s"Executor with id $execId was not detected in the Kubernetes" +
s" cluster after $podCreationTimeout milliseconds despite the fact that a" +
" previous allocation attempt tried to create it. The executor may have been" +
" deleted but the application missed the deletion event.")
Utils.tryLogNonFatalError {
kubernetesClient
.pods()
.withLabel(SPARK_EXECUTOR_ID_LABEL, execId.toString)
.delete()
Copy link
Contributor

@skonto skonto Jun 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't removeExecutorFromSpark be called here as well? Couldn't be the case that the executor exists at a higher level but K8s backend missed it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's handled by the lifecycle manager already, because the lifecycle manager looks at what the scheduler backend believes are its executors and reconciles them with what's in the snapshot.

}
newlyCreatedExecutors -= execId
} else {
logDebug(s"Executor with id $execId was not found in the Kubernetes cluster since it" +
s" was created ${currentTime - timeCreated} milliseconds ago.")
}
}

if (snapshots.nonEmpty) {
// Only need to examine the cluster as of the latest snapshot, the "current" state, to see if
// we need to allocate more executors or not.
val latestSnapshot = snapshots.last
val currentRunningExecutors = latestSnapshot.executorPods.values.count {
case PodRunning(_) => true
case _ => false
}
val currentPendingExecutors = latestSnapshot.executorPods.values.count {
case PodPending(_) => true
case _ => false
}
val currentTotalExpectedExecutors = totalExpectedExecutors.get
logDebug(s"Currently have $currentRunningExecutors running executors and" +
s" $currentPendingExecutors pending executors. $newlyCreatedExecutors executors" +
s" have been requested but are pending appearance in the cluster.")
if (newlyCreatedExecutors.isEmpty
&& currentPendingExecutors == 0
&& currentRunningExecutors < currentTotalExpectedExecutors) {
val numExecutorsToAllocate = math.min(
currentTotalExpectedExecutors - currentRunningExecutors, podAllocationSize)
logInfo(s"Going to request $numExecutorsToAllocate executors from Kubernetes.")
for ( _ <- 0 until numExecutorsToAllocate) {
val newExecutorId = EXECUTOR_ID_COUNTER.incrementAndGet()
val executorConf = KubernetesConf.createExecutorConf(
conf,
newExecutorId.toString,
applicationId,
driverPod)
val executorPod = executorBuilder.buildFromFeatures(executorConf)
val podWithAttachedContainer = new PodBuilder(executorPod.pod)
.editOrNewSpec()
.addToContainers(executorPod.container)
.endSpec()
.build()
kubernetesClient.pods().create(podWithAttachedContainer)
newlyCreatedExecutors(newExecutorId) = clock.getTimeMillis()
logDebug(s"Requested executor with id $newExecutorId from Kubernetes.")
}
} else if (currentRunningExecutors >= currentTotalExpectedExecutors) {
// TODO handle edge cases if we end up with more running executors than expected.
logDebug("Current number of running executors is equal to the number of requested" +
" executors. Not scaling up further.")
} else if (newlyCreatedExecutors.nonEmpty || currentPendingExecutors != 0) {
logDebug(s"Still waiting for ${newlyCreatedExecutors.size + currentPendingExecutors}" +
s" executors to begin running before requesting for more executors. # of executors in" +
s" pending status in the cluster: $currentPendingExecutors. # of executors that we have" +
s" created but we have not observed as being present in the cluster yet:" +
s" ${newlyCreatedExecutors.size}.")
}
}
}
}
Loading