-
Notifications
You must be signed in to change notification settings - Fork 28.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SPARK-4180] [Core] Prevent creation of multiple active SparkContexts #3121
Conversation
Test build #22959 has started for PR 3121 at commit
|
Oh, and /cc @mateiz and @andrewor14, too. |
Test build #22960 has started for PR 3121 at commit
|
Test build #22959 has finished for PR 3121 at commit
|
Test FAILed. |
Test build #22960 has finished for PR 3121 at commit
|
Test FAILed. |
This failed a bunch of tests because Spark Streaming's |
Test build #22975 has started for PR 3121 at commit
|
Test build #22975 has finished for PR 3121 at commit
|
Test FAILed. |
@tdas It looks like a change that I made to
Here's the test code: test("stop before start and start after stop") {
ssc = new StreamingContext(master, appName, batchDuration)
addInputStream(ssc).register()
ssc.stop() // stop before start should not throw exception
ssc.start() // <---- This is the line that's throwing the exception
ssc.stop()
intercept[SparkException] {
ssc.start() // start after stop should throw exception
}
} The issue is that the old code would let you call |
Here's another example of bad test fixture design: Spark SQL's /** A SQLContext that can be used for local testing. */
object TestSQLContext
extends SQLContext(
new SparkContext(
"local[2]",
"TestSQLContext",
new SparkConf().set("spark.sql.testkey", "true"))) {
/** Fewer partitions to speed up testing. */
override private[spark] def numShufflePartitions: Int =
getConf(SQLConf.SHUFFLE_PARTITIONS, "5").toInt
} I'm going to refactor this into a proper testing trait, |
This SQL situation is going to be a huge mess to fix and I fear that I'll wind up having to touch nearly every line of test code (or at least a substantial fraction of them). Part of the problem is that there are also global class CachedTableSuite extends QueryTest {
TestData // Load test tables.
class InMemoryColumnarQuerySuite extends QueryTest {
// Make sure the tables are loaded.
TestData I want to avoid rewriting the bulk of the actual test logic. One approach is to leave these as global objects but convert their fields to Perhaps this design using global objects that are shared across test suites was motivated by performance concerns, since it might be expensive to create a bunch of tables. I think that the right approach to addressing test performance is through coarse-grained parallelization by running individual test suites in separate JVMs, since global shared state can be confusing. Also, I think the performance impact might be fairly minimal: we'd only be re-initializing once per suite, not once per test. @marmbrus, do you have any feedback here? Is there a cleaner way to enable cleanup of the SparkContext that the SQL tests create? |
To be more concrete, I'm suggesting something like this: object TestData {
/**
* Initialize TestData using the given SQLContext. This will re-create all SchemaRDDs and tables
* using that context.
*/
def init(sqlContext: SQLContext) {
initMethods.foreach(m => m(sqlContext))
}
/** A sequence of functions that are invoked when `init()` is called */
private val initMethods = mutable.Buffer[SQLContext => Unit]()
/**
* Register a block of code to be called when TestData is initialized with a new SQLContext.
*/
private def onInit(block: SQLContext => Unit) {
initMethods += block
}
def testData = _testData
private var _testData: SchemaRDD = null
onInit { sqlContext =>
_testData = sqlContext.sparkContext.parallelize(
(1 to 100).map(i => TestData(i, i.toString))).toSchemaRDD
testData.registerTempTable("testData")
}
case class LargeAndSmallInts(a: Int, b: Int)
def largeAndSmallInts = _largeAndSmallInts
private var _largeAndSmallInts: SchemaRDD = null
onInit { sqlContext =>
...
}
[...] This whole |
Here is the prior discussion on the issue you brought up with StreamingSparkContext: #3053 (comment) Note that the result was the reversion of the change and the addition of this check: |
@JoshRosen I don't think the situation is quite a dire as you suggest (every line of test code?). We can add logic to Hive is going to be another story. The whole reason for this singleton context pattern is that we have problems initializing more than one HiveContext in a single JVM. If you try to do that all DDL operations fail with a mysterious |
The one subtlety with mixing in One alternative is to have the mixin trait be "resettable" by exposing |
} | ||
val exception = new SparkException(s"$errMsg $errDetails") | ||
if (conf.getBoolean("spark.driver.disableMultipleSparkContextsErrorChecking", false)) { | ||
logWarning("Multiple SparkContext error detection is disabled!", exception) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More like "Multiple SparkContexts detected in the same JVM!". We're not trying to warn people that they disabled the config.
@aarondav I just noticed another test failure which was due to the same problem that you saw in #3053: some of our tests throw exceptions when creating SparkContexts and then never clean up those resources, which triggers the "cannot create a new SparkContext if a previous attempt failed" limitation that this commit introduces. I'd rather not refactor a bunch of test code in SQL, so I'm probably going to just add the "turn this error into a warning" flag to our test Java options; we can do the final cleanup / refactoring later. |
I've made another pass which I think should address this last round of review feedback. Thanks for all of the careful review and commentary so far. |
Test build #23418 has started for PR 3121 at commit
|
Test build #23418 has finished for PR 3121 at commit
|
Test PASSed. |
@@ -57,12 +57,27 @@ import org.apache.spark.util._ | |||
* Main entry point for Spark functionality. A SparkContext represents the connection to a Spark | |||
* cluster, and can be used to create RDDs, accumulators and broadcast variables on that cluster. | |||
* | |||
* Only one SparkContext may be active per JVM. You must `stop()` the active SparkContext before | |||
* creating a new one. This limitation will eventually be removed; see SPARK-2243 for more details. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say "may eventually be removed".
This is a nice solution and LGTM. Made only minor comments. |
Conflicts: core/src/main/scala/org/apache/spark/SparkContext.scala
Test build #23457 has started for PR 3121 at commit
|
Test build #23457 has finished for PR 3121 at commit
|
Test PASSed. |
LGTM |
Thanks Josh - I'm gonna pull this in. |
Hey Josh - I've put this into branch 1.2. Since this is a nontrivial patch I'm sort of hesitant to throw it directly in branch-1.1 right now. Could we wait an backport this after 1.1.1 ships? |
This patch adds error-detection logic to throw an exception when attempting to create multiple active SparkContexts in the same JVM, since this is currently unsupported and has been known to cause confusing behavior (see SPARK-2243 for more details). **The solution implemented here is only a partial fix.** A complete fix would have the following properties: 1. Only one SparkContext may ever be under construction at any given time. 2. Once a SparkContext has been successfully constructed, any subsequent construction attempts should fail until the active SparkContext is stopped. 3. If the SparkContext constructor throws an exception, then all resources created in the constructor should be cleaned up (SPARK-4194). 4. If a user attempts to create a SparkContext but the creation fails, then the user should be able to create new SparkContexts. This PR only provides 2) and 4); we should be able to provide all of these properties, but the correct fix will involve larger changes to SparkContext's construction / initialization, so we'll target it for a different Spark release. ### The correct solution: I think that the correct way to do this would be to move the construction of SparkContext's dependencies into a static method in the SparkContext companion object. Specifically, we could make the default SparkContext constructor `private` and change it to accept a `SparkContextDependencies` object that contains all of SparkContext's dependencies (e.g. DAGScheduler, ContextCleaner, etc.). Secondary constructors could call a method on the SparkContext companion object to create the `SparkContextDependencies` and pass the result to the primary SparkContext constructor. For example: ```scala class SparkContext private (deps: SparkContextDependencies) { def this(conf: SparkConf) { this(SparkContext.getDeps(conf)) } } object SparkContext( private[spark] def getDeps(conf: SparkConf): SparkContextDependencies = synchronized { if (anotherSparkContextIsActive) { throw Exception(...) } var dagScheduler: DAGScheduler = null try { dagScheduler = new DAGScheduler(...) [...] } catch { case e: Exception => Option(dagScheduler).foreach(_.stop()) [...] } SparkContextDependencies(dagScheduler, ....) } } ``` This gives us mutual exclusion and ensures that any resources created during the failed SparkContext initialization are properly cleaned up. This indirection is necessary to maintain binary compatibility. In retrospect, it would have been nice if SparkContext had no private constructors and could only be created through builder / factory methods on its companion object, since this buys us lots of flexibility and makes dependency injection easier. ### Alternative solutions: As an alternative solution, we could refactor SparkContext's primary constructor to perform all object creation in a giant `try-finally` block. Unfortunately, this will require us to turn a bunch of `vals` into `vars` so that they can be assigned from the `try` block. If we still want `vals`, we could wrap each `val` in its own `try` block (since the try block can return a value), but this will lead to extremely messy code and won't guard against the introduction of future code which doesn't properly handle failures. The more complex approach outlined above gives us some nice dependency injection benefits, so I think that might be preferable to a `var`-ification. ### This PR's solution: - At the start of the constructor, check whether some other SparkContext is active; if so, throw an exception. - If another SparkContext might be under construction (or has thrown an exception during construction), allow the new SparkContext to begin construction but log a warning (since resources might have been leaked from a failed creation attempt). - At the end of the SparkContext constructor, check whether some other SparkContext constructor has raced and successfully created an active context. If so, throw an exception. This guarantees that no two SparkContexts will ever be active and exposed to users (since we check at the very end of the constructor). If two threads race to construct SparkContexts, then one of them will win and another will throw an exception. This exception can be turned into a warning by setting `spark.driver.allowMultipleContexts = true`. The exception is disabled in unit tests, since there are some suites (such as Hive) that may require more significant refactoring to clean up their SparkContexts. I've made a few changes to other suites' test fixtures to properly clean up SparkContexts so that the unit test logs contain fewer warnings. Author: Josh Rosen <joshrosen@databricks.com> Closes #3121 from JoshRosen/SPARK-4180 and squashes the following commits: 23c7123 [Josh Rosen] Merge remote-tracking branch 'origin/master' into SPARK-4180 d38251b [Josh Rosen] Address latest round of feedback. c0987d3 [Josh Rosen] Accept boolean instead of SparkConf in methods. 85a424a [Josh Rosen] Incorporate more review feedback. 372d0d3 [Josh Rosen] Merge remote-tracking branch 'origin/master' into SPARK-4180 f5bb78c [Josh Rosen] Update mvn build, too. d809cb4 [Josh Rosen] Improve handling of failed SparkContext creation attempts. 79a7e6f [Josh Rosen] Fix commented out test a1cba65 [Josh Rosen] Merge remote-tracking branch 'origin/master' into SPARK-4180 7ba6db8 [Josh Rosen] Add utility to set system properties in tests. 4629d5c [Josh Rosen] Set spark.driver.allowMultipleContexts=true in tests. ed17e14 [Josh Rosen] Address review feedback; expose hack workaround for existing unit tests. 1c66070 [Josh Rosen] Merge remote-tracking branch 'origin/master' into SPARK-4180 06c5c54 [Josh Rosen] Add / improve SparkContext cleanup in streaming BasicOperationsSuite d0437eb [Josh Rosen] StreamingContext.stop() should stop SparkContext even if StreamingContext has not been started yet. c4d35a2 [Josh Rosen] Log long form of creation site to aid debugging. 918e878 [Josh Rosen] Document "one SparkContext per JVM" limitation. afaa7e3 [Josh Rosen] [SPARK-4180] Prevent creations of multiple active SparkContexts. (cherry picked from commit 0f3ceb5) Signed-off-by: Patrick Wendell <pwendell@gmail.com>
Sure; I've removed 1.1.1 as a target version and added 1.1.2. |
This patch adds error-detection logic to throw an exception when attempting to create multiple active SparkContexts in the same JVM, since this is currently unsupported and has been known to cause confusing behavior (see SPARK-2243 for more details).
The solution implemented here is only a partial fix. A complete fix would have the following properties:
This PR only provides 2) and 4); we should be able to provide all of these properties, but the correct fix will involve larger changes to SparkContext's construction / initialization, so we'll target it for a different Spark release.
The correct solution:
I think that the correct way to do this would be to move the construction of SparkContext's dependencies into a static method in the SparkContext companion object. Specifically, we could make the default SparkContext constructor
private
and change it to accept aSparkContextDependencies
object that contains all of SparkContext's dependencies (e.g. DAGScheduler, ContextCleaner, etc.). Secondary constructors could call a method on the SparkContext companion object to create theSparkContextDependencies
and pass the result to the primary SparkContext constructor. For example:This gives us mutual exclusion and ensures that any resources created during the failed SparkContext initialization are properly cleaned up.
This indirection is necessary to maintain binary compatibility. In retrospect, it would have been nice if SparkContext had no private constructors and could only be created through builder / factory methods on its companion object, since this buys us lots of flexibility and makes dependency injection easier.
Alternative solutions:
As an alternative solution, we could refactor SparkContext's primary constructor to perform all object creation in a giant
try-finally
block. Unfortunately, this will require us to turn a bunch ofvals
intovars
so that they can be assigned from thetry
block. If we still wantvals
, we could wrap eachval
in its owntry
block (since the try block can return a value), but this will lead to extremely messy code and won't guard against the introduction of future code which doesn't properly handle failures.The more complex approach outlined above gives us some nice dependency injection benefits, so I think that might be preferable to a
var
-ification.This PR's solution:
This guarantees that no two SparkContexts will ever be active and exposed to users (since we check at the very end of the constructor). If two threads race to construct SparkContexts, then one of them will win and another will throw an exception.
This exception can be turned into a warning by setting
spark.driver.allowMultipleContexts = true
. The exception is disabled in unit tests, since there are some suites (such as Hive) that may require more significant refactoring to clean up their SparkContexts. I've made a few changes to other suites' test fixtures to properly clean up SparkContexts so that the unit test logs contain fewer warnings.