Skip to content

Commit a7c25e5

Browse files
authored
Merge pull request #83 from scala-exercises/enrique-0-5-0-memory-fix
Classloader leak fix
2 parents 523d7b7 + 86d8975 commit a7c25e5

File tree

2 files changed

+51
-50
lines changed

2 files changed

+51
-50
lines changed

server/src/main/scala/org/scalaexercises/evaluator/evaluation.scala

+46-44
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77

88
package org.scalaexercises.evaluator
99

10-
import java.io.{ByteArrayOutputStream, File}
10+
import java.io.{ByteArrayOutputStream, Closeable, File}
1111
import java.math.BigInteger
12-
import java.net.URLClassLoader
1312
import java.security.MessageDigest
1413
import java.util.jar.JarFile
1514

@@ -19,10 +18,12 @@ import coursier._
1918
import coursier.cache.{ArtifactError, FileCache}
2019
import coursier.util.Sync
2120
import org.scalaexercises.evaluator.Eval.CompilerException
21+
import org.scalaexercises.evaluator.{Dependency => EvaluatorDependency}
2222

2323
import scala.concurrent.duration._
2424
import scala.language.reflectiveCalls
2525
import scala.reflect.internal.util.{AbstractFileClassLoader, BatchSourceFile, Position}
26+
import scala.reflect.internal.util.ScalaClassLoader.URLClassLoader
2627
import scala.tools.nsc.io.{AbstractFile, VirtualDirectory}
2728
import scala.tools.nsc.reporters._
2829
import scala.tools.nsc.{Global, Settings}
@@ -60,7 +61,9 @@ class Evaluator[F[_]: Sync](timeout: FiniteDuration = 20.seconds)(
6061

6162
val cache: FileCache[F] = FileCache[F].noCredentials
6263

63-
def resolveArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): F[Resolution] = {
64+
def resolveArtifacts(
65+
remotes: Seq[Remote],
66+
dependencies: Seq[EvaluatorDependency]): F[Resolution] = {
6467
Resolve[F](cache)
6568
.addDependencies(dependencies.map(dependencyToModule): _*)
6669
.addRepositories(remotes.map(remoteToRepository): _*)
@@ -70,7 +73,7 @@ class Evaluator[F[_]: Sync](timeout: FiniteDuration = 20.seconds)(
7073

7174
def fetchArtifacts(
7275
remotes: Seq[Remote],
73-
dependencies: Seq[Dependency]): F[Either[ArtifactError, List[File]]] =
76+
dependencies: Seq[EvaluatorDependency]): F[Either[ArtifactError, List[File]]] =
7477
for {
7578
resolution <- resolveArtifacts(remotes, dependencies)
7679
gatheredArtifacts <- resolution.artifacts().toList.traverse(cache.file(_).run)
@@ -109,43 +112,41 @@ class Evaluator[F[_]: Sync](timeout: FiniteDuration = 20.seconds)(
109112
}
110113
}
111114

112-
private[this] def evaluate[T](code: String, jars: Seq[File]): EvalResult[T] = {
113-
val eval = createEval(jars)
115+
private[this] def evaluate[T](code: String, jars: Seq[File]): F[EvalResult[T]] = {
116+
F.bracket(F.delay(createEval(jars))) { evalInstance =>
117+
val outCapture = new ByteArrayOutputStream
114118

115-
val outCapture = new ByteArrayOutputStream
119+
F.delay[EvalResult[T]](Console.withOut(outCapture) {
116120

117-
Console.withOut(outCapture) {
121+
val result = Try(evalInstance.execute[T](code, resetState = true, jars = jars))
118122

119-
val result = for {
120-
_ Try(eval.check(code))
121-
result Try(eval.execute[T](code, resetState = true, jars = jars))
122-
} yield result
123+
val errors = evalInstance.errors
123124

124-
val errors = eval.errors
125+
result match {
126+
case scala.util.Success(r) => EvalSuccess[T](errors, r, outCapture.toString)
127+
case scala.util.Failure(t) =>
128+
t match {
129+
case e: CompilerException => CompilationError(errors)
130+
case NonFatal(e) =>
131+
EvalRuntimeError(errors, Option(RuntimeError(e, None)))
132+
case e => GeneralError(e)
133+
}
134+
}
135+
})
136+
}(EI => F.delay(EI.clean()))
125137

126-
result match {
127-
case scala.util.Success(r) EvalSuccess[T](errors, r, outCapture.toString)
128-
case scala.util.Failure(t)
129-
t match {
130-
case e: CompilerException CompilationError(errors)
131-
case NonFatal(e)
132-
EvalRuntimeError(errors, Option(RuntimeError(e, None)))
133-
case e GeneralError(e)
134-
}
135-
}
136-
}
137138
}
138139

139140
def eval[T](
140141
code: String,
141142
remotes: Seq[Remote] = Nil,
142-
dependencies: Seq[Dependency] = Nil
143+
dependencies: Seq[EvaluatorDependency] = Nil
143144
): F[EvalResult[T]] = {
144145
for {
145146
allJars <- fetchArtifacts(remotes, dependencies)
146147
result <- allJars match {
147148
case Right(jars) =>
148-
timeoutTo[EvalResult[T]](F.delay { evaluate(code, jars) }, timeout, Timeout[T](timeout))
149+
timeoutTo[EvalResult[T]](evaluate(code, jars), timeout, Timeout[T](timeout))
149150
case Left(fileError) => F.pure(UnresolvedDependency[T](fileError.describe))
150151
}
151152
} yield result
@@ -176,7 +177,7 @@ private class StringCompiler(
176177
output: AbstractFile,
177178
settings: Settings,
178179
messageHandler: Option[Reporter]
179-
) {
180+
) extends Closeable {
180181

181182
val cache = new scala.collection.mutable.HashMap[String, Class[_]]()
182183

@@ -290,6 +291,12 @@ private class StringCompiler(
290291
findClass(className, classLoader).get // fixme
291292
}
292293
}
294+
295+
override def close(): Unit = {
296+
global.cleanup
297+
global.close()
298+
reporter.reset()
299+
}
293300
}
294301

295302
/**
@@ -358,37 +365,32 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) {
358365
}
359366

360367
def execute[T](className: String, code: String, resetState: Boolean, jars: Seq[File]): T = {
361-
val jarUrls = jars
362-
.map(jar => new java.net.URL(s"file://${jar.getAbsolutePath}"))
363-
.toArray
364-
val urlClassLoader =
365-
new URLClassLoader(jarUrls, compiler.getClass.getClassLoader)
366-
val classLoader =
367-
new AbstractFileClassLoader(compilerOutputDir, urlClassLoader)
368+
val jarUrls = jars.map(jar => new java.net.URL(s"file://${jar.getAbsolutePath}"))
369+
val urlClassLoader = new URLClassLoader(jarUrls, compiler.getClass.getClassLoader)
370+
val classLoader = new AbstractFileClassLoader(compilerOutputDir, urlClassLoader)
368371

369372
val cls = compiler(
370373
wrapCodeInClass(className, code),
371374
className,
372375
resetState,
373376
classLoader
374377
)
375-
cls
378+
379+
val res = cls
376380
.getConstructor()
377381
.newInstance()
378382
.asInstanceOf[() => T]
379383
.apply()
380384
.asInstanceOf[T]
385+
386+
urlClassLoader.close()
387+
388+
res
381389
}
382390

383-
/**
384-
* Check if code is Eval-able.
385-
* @throws CompilerException if not Eval-able.
386-
*/
387-
def check(code: String) = {
388-
val id = uniqueId(code)
389-
val className = "Evaluator__" + id
390-
val wrappedCode = wrapCodeInClass(className, code)
391-
compiler(wrappedCode)
391+
def clean(): Unit = {
392+
compiler.close()
393+
compilerMessageHandler.foreach(_.reset())
392394
}
393395

394396
private[this] def uniqueId(code: String, idOpt: Option[Int] = Some(Eval.jvmId)): String = {

server/src/main/scala/org/scalaexercises/evaluator/services.scala

+5-6
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import org.log4s.getLogger
2222
import org.scalaexercises.evaluator.codecs._
2323

2424
import scala.concurrent.duration._
25-
import scala.language.postfixOps
2625

2726
object services {
2827

@@ -113,16 +112,16 @@ object EvaluatorServer extends IOApp {
113112
lazy val port = (Option(System.getenv("PORT")) orElse
114113
Option(System.getProperty("http.port"))).map(_.toInt).getOrElse(8080)
115114

116-
lazy val evaluator = new Evaluator[IO](15 seconds)
115+
val httpApp = auth[IO](service(new Evaluator[IO](15.seconds)))
117116

118117
override def run(args: List[String]): IO[ExitCode] = {
119118
logger.info(s"Initializing Evaluator at $ip:$port")
120119

121120
BlazeServerBuilder[IO]
122121
.bindHttp(port, ip)
123-
.withHttpApp(auth[IO](service(evaluator)))
124-
.serve
125-
.compile
126-
.lastOrError
122+
.withHttpApp(httpApp)
123+
.resource
124+
.use(_ => IO.never)
125+
.as(ExitCode.Success)
127126
}
128127
}

0 commit comments

Comments
 (0)