17
17
18
18
package org .apache .spark .repl
19
19
20
- import java .io .File
20
+ import java .io .{File , IOException }
21
+ import java .lang .reflect .InvocationTargetException
21
22
import java .net .{URI , URL , URLClassLoader }
22
- import java .nio .channels .FileChannel
23
+ import java .nio .channels .{ FileChannel , ReadableByteChannel }
23
24
import java .nio .charset .StandardCharsets
24
25
import java .nio .file .{Paths , StandardOpenOption }
25
26
import java .util
@@ -30,13 +31,15 @@ import scala.io.Source
30
31
import scala .language .implicitConversions
31
32
32
33
import com .google .common .io .Files
33
- import org .mockito .ArgumentMatchers .anyString
34
+ import org .mockito .ArgumentMatchers .{ any , anyString }
34
35
import org .mockito .Mockito ._
35
36
import org .mockito .invocation .InvocationOnMock
37
+ import org .mockito .stubbing .Answer
36
38
import org .scalatest .BeforeAndAfterAll
37
39
import org .scalatest .mockito .MockitoSugar
38
40
39
41
import org .apache .spark ._
42
+ import org .apache .spark .TestUtils .JavaSourceFromString
40
43
import org .apache .spark .internal .Logging
41
44
import org .apache .spark .rpc .RpcEnv
42
45
import org .apache .spark .util .Utils
@@ -193,7 +196,14 @@ class ExecutorClassLoaderSuite
193
196
when(rpcEnv.openChannel(anyString())).thenAnswer((invocation : InvocationOnMock ) => {
194
197
val uri = new URI (invocation.getArguments()(0 ).asInstanceOf [String ])
195
198
val path = Paths .get(tempDir1.getAbsolutePath(), uri.getPath().stripPrefix(" /" ))
196
- FileChannel .open(path, StandardOpenOption .READ )
199
+ if (path.toFile.exists()) {
200
+ FileChannel .open(path, StandardOpenOption .READ )
201
+ } else {
202
+ val channel = mock[ReadableByteChannel ]
203
+ when(channel.read(any()))
204
+ .thenThrow(new RuntimeException (s " Stream ' ${uri.getPath}' was not found. " ))
205
+ channel
206
+ }
197
207
})
198
208
199
209
val classLoader = new ExecutorClassLoader (new SparkConf (), env, " spark://localhost:1234" ,
@@ -218,4 +228,131 @@ class ExecutorClassLoaderSuite
218
228
}
219
229
}
220
230
231
+ test(" nonexistent class and transient errors should cause different errors" ) {
232
+ val conf = new SparkConf ()
233
+ .setMaster(" local" )
234
+ .setAppName(" executor-class-loader-test" )
235
+ .set(" spark.network.timeout" , " 11s" )
236
+ .set(" spark.repl.class.outputDir" , tempDir1.getAbsolutePath)
237
+ val sc = new SparkContext (conf)
238
+ try {
239
+ val replClassUri = sc.conf.get(" spark.repl.class.uri" )
240
+
241
+ // Create an RpcEnv for executor
242
+ val rpcEnv = RpcEnv .create(
243
+ SparkEnv .executorSystemName,
244
+ " localhost" ,
245
+ " localhost" ,
246
+ 0 ,
247
+ sc.conf,
248
+ new SecurityManager (conf), 0 , clientMode = true )
249
+
250
+ try {
251
+ val env = mock[SparkEnv ]
252
+ when(env.rpcEnv).thenReturn(rpcEnv)
253
+
254
+ val classLoader = new ExecutorClassLoader (
255
+ conf,
256
+ env,
257
+ replClassUri,
258
+ getClass().getClassLoader(),
259
+ false )
260
+
261
+ // Test loading a nonexistent class
262
+ intercept[java.lang.ClassNotFoundException ] {
263
+ classLoader.loadClass(" NonexistentClass" )
264
+ }
265
+
266
+ // Stop SparkContext to simulate transient errors in executors
267
+ sc.stop()
268
+
269
+ val e = intercept[RemoteClassLoaderError ] {
270
+ classLoader.loadClass(" ThisIsAClassName" )
271
+ }
272
+ assert(e.getMessage.contains(" ThisIsAClassName" ))
273
+ // RemoteClassLoaderError must not be LinkageError nor ClassNotFoundException. Otherwise,
274
+ // JVM will cache it and doesn't retry to load a class.
275
+ assert(! e.isInstanceOf [LinkageError ] && ! e.isInstanceOf [ClassNotFoundException ])
276
+ } finally {
277
+ rpcEnv.shutdown()
278
+ rpcEnv.awaitTermination()
279
+ }
280
+ } finally {
281
+ sc.stop()
282
+ }
283
+ }
284
+
285
+ test(" SPARK-20547 ExecutorClassLoader should not throw ClassNotFoundException without " +
286
+ " acknowledgment from driver" ) {
287
+ val tempDir = Utils .createTempDir()
288
+ try {
289
+ // Create two classes, "TestClassB" calls "TestClassA", so when calling "TestClassB.foo", JVM
290
+ // will try to load "TestClassA".
291
+ val sourceCodeOfClassA =
292
+ """ public class TestClassA implements java.io.Serializable {
293
+ | @Override public String toString() { return "TestClassA"; }
294
+ |}""" .stripMargin
295
+ val sourceFileA = new JavaSourceFromString (" TestClassA" , sourceCodeOfClassA)
296
+ TestUtils .createCompiledClass(
297
+ sourceFileA.name, tempDir, sourceFileA, Seq (tempDir.toURI.toURL))
298
+
299
+ val sourceCodeOfClassB =
300
+ """ public class TestClassB implements java.io.Serializable {
301
+ | public String foo() { return new TestClassA().toString(); }
302
+ | @Override public String toString() { return "TestClassB"; }
303
+ |}""" .stripMargin
304
+ val sourceFileB = new JavaSourceFromString (" TestClassB" , sourceCodeOfClassB)
305
+ TestUtils .createCompiledClass(
306
+ sourceFileB.name, tempDir, sourceFileB, Seq (tempDir.toURI.toURL))
307
+
308
+ val env = mock[SparkEnv ]
309
+ val rpcEnv = mock[RpcEnv ]
310
+ when(env.rpcEnv).thenReturn(rpcEnv)
311
+ when(rpcEnv.openChannel(anyString())).thenAnswer(new Answer [ReadableByteChannel ]() {
312
+ private var count = 0
313
+
314
+ override def answer (invocation : InvocationOnMock ): ReadableByteChannel = {
315
+ val uri = new URI (invocation.getArguments()(0 ).asInstanceOf [String ])
316
+ val classFileName = uri.getPath().stripPrefix(" /" )
317
+ if (count == 0 && classFileName == " TestClassA.class" ) {
318
+ count += 1
319
+ // Let the first attempt to load TestClassA fail with an IOException
320
+ val channel = mock[ReadableByteChannel ]
321
+ when(channel.read(any())).thenThrow(new IOException (" broken pipe" ))
322
+ channel
323
+ }
324
+ else {
325
+ val path = Paths .get(tempDir.getAbsolutePath(), classFileName)
326
+ FileChannel .open(path, StandardOpenOption .READ )
327
+ }
328
+ }
329
+ })
330
+
331
+ val classLoader = new ExecutorClassLoader (new SparkConf (), env, " spark://localhost:1234" ,
332
+ getClass().getClassLoader(), false )
333
+
334
+ def callClassBFoo (): String = {
335
+ // scalastyle:off classforname
336
+ val classB = Class .forName(" TestClassB" , true , classLoader)
337
+ // scalastyle:on classforname
338
+ val instanceOfTestClassB = classB.newInstance()
339
+ assert(instanceOfTestClassB.toString === " TestClassB" )
340
+ classB.getMethod(" foo" ).invoke(instanceOfTestClassB).asInstanceOf [String ]
341
+ }
342
+
343
+ // Reflection will wrap the exception with InvocationTargetException
344
+ val e = intercept[InvocationTargetException ] {
345
+ callClassBFoo()
346
+ }
347
+ // "TestClassA" cannot be loaded because of IOException
348
+ assert(e.getCause.isInstanceOf [RemoteClassLoaderError ])
349
+ assert(e.getCause.getCause.isInstanceOf [IOException ])
350
+ assert(e.getCause.getMessage.contains(" TestClassA" ))
351
+
352
+ // We should be able to re-load TestClassA for IOException
353
+ assert(callClassBFoo() === " TestClassA" )
354
+ } finally {
355
+ Utils .deleteRecursively(tempDir)
356
+ }
357
+ }
221
358
}
0 commit comments