Skip to content

Commit 31d29fa

Browse files
committed
Added memory leak fixes
1 parent 6088c19 commit 31d29fa

File tree

7 files changed

+98
-76
lines changed

7 files changed

+98
-76
lines changed

.sbtopts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-Dscala.classpath.closeZip=true

build.sbt

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
Universal / javaOptions += "-Dscala.classpath.closeZip=true"
2+
13
lazy val `evaluator-server` = (project in file("server"))
24
.enablePlugins(JavaAppPackaging)
35
.enablePlugins(AutomateHeaderPlugin)

project/ProjectPlugin.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ object ProjectPlugin extends AutoPlugin {
115115
scalaVersion := "2.13.1",
116116
scalaOrganization := "org.scala-lang",
117117
javacOptions ++= Seq("-encoding", "UTF-8", "-Xlint:-options"),
118-
fork in Test := false,
118+
//javaOptions += "-Dscala.classpath.closeZip=true",
119+
//fork in Test := false,
119120
parallelExecution in Test := false,
120121
cancelable in Global := true,
121122
headerLicense := Some(

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

+86-65
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

@@ -24,10 +23,11 @@ import org.scalaexercises.evaluator.{Dependency => EvaluatorDependency}
2423

2524
import scala.concurrent.duration._
2625
import scala.reflect.internal.util.{AbstractFileClassLoader, BatchSourceFile, Position}
26+
import scala.reflect.internal.util.ScalaClassLoader.URLClassLoader
2727
import scala.tools.nsc.io.{AbstractFile, VirtualDirectory}
2828
import scala.tools.nsc.reporters._
2929
import scala.tools.nsc.{Global, Settings}
30-
import scala.util.Try
30+
import scala.util.{Failure, Success, Try}
3131
import scala.util.control.NonFatal
3232

3333
class Evaluator[F[_]: Sync](timeout: FiniteDuration = 20.seconds)(
@@ -83,7 +83,15 @@ class Evaluator[F[_]: Sync](timeout: FiniteDuration = 20.seconds)(
8383
}
8484
} yield artifacts
8585

86-
def createEval(jars: Seq[File]) = {
86+
def fetch(remotes: Seq[Remote], dependencies: Seq[EvaluatorDependency]) =
87+
Fetch[F](cache)
88+
.addDependencies(dependencies.map(dependencyToModule): _*)
89+
.addRepositories(remotes.map(remoteToRepository): _*)
90+
.addRepositories(coursier.LocalRepositories.ivy2Local)
91+
.io
92+
.attempt
93+
94+
def createEval(jars: Seq[File]) =
8795
new Eval(jars = jars.toList) {
8896
@volatile var errors: Map[String, List[CompilationInfo]] = Map.empty
8997

@@ -110,34 +118,32 @@ class Evaluator[F[_]: Sync](timeout: FiniteDuration = 20.seconds)(
110118
errors = Map.empty
111119
}
112120
})
121+
113122
}
114-
}
115123

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

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

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

123-
val result = for {
124-
_ <- Try(eval.check(code))
125-
result <- Try(eval.execute[T](code, resetState = true, jars = jars))
126-
} yield result
132+
val errors = evalInstance.errors
127133

128-
val errors = eval.errors
134+
result match {
135+
case scala.util.Success(r) => EvalSuccess[T](errors, r, outCapture.toString)
136+
case scala.util.Failure(t) =>
137+
t match {
138+
case e: CompilerException => CompilationError(errors)
139+
case NonFatal(e) =>
140+
EvalRuntimeError(errors, Option(RuntimeError(e, None)))
141+
case e => GeneralError(e)
142+
}
143+
}
144+
})
145+
}(EI => F.delay(EI.clean()))
129146

130-
result match {
131-
case scala.util.Success(r) => EvalSuccess[T](errors, r, outCapture.toString)
132-
case scala.util.Failure(t) =>
133-
t match {
134-
case e: CompilerException => CompilationError(errors)
135-
case NonFatal(e) =>
136-
EvalRuntimeError(errors, Option(RuntimeError(e, None)))
137-
case e => GeneralError(e)
138-
}
139-
}
140-
}
141147
}
142148

143149
def eval[T](
@@ -149,7 +155,7 @@ class Evaluator[F[_]: Sync](timeout: FiniteDuration = 20.seconds)(
149155
allJars <- fetchArtifacts(remotes, dependencies)
150156
result <- allJars match {
151157
case Right(jars) =>
152-
F.delay[EvalResult[T]](evaluate(code, jars))
158+
evaluate(code, jars)
153159
.timeoutTo(timeout, Timeout[T](timeout).asInstanceOf[EvalResult[T]].pure[F])
154160
case Left(fileError) => F.pure(UnresolvedDependency[T](fileError.describe))
155161
}
@@ -173,9 +179,9 @@ private class StringCompiler(
173179
output: AbstractFile,
174180
settings: Settings,
175181
messageHandler: Option[Reporter]
176-
) {
182+
) extends Closeable {
177183

178-
val cache = new scala.collection.mutable.HashMap[String, Class[_]]()
184+
//val cache = new java.util.concurrent.ConcurrentHashMap[String, Class[_]]
179185

180186
trait MessageCollector {
181187
val messages: scala.collection.mutable.ListBuffer[List[String]]
@@ -229,23 +235,28 @@ private class StringCompiler(
229235
}
230236
}
231237
}
232-
cache.clear()
238+
global.cleanup
239+
//cache.clear()
233240
reporter.reset()
234241
}
235242

236-
def findClass(className: String, classLoader: ClassLoader): Option[Class[_]] = {
237-
synchronized {
238-
cache.get(className).orElse {
239-
try {
240-
val cls = classLoader.loadClass(className)
241-
cache(className) = cls
242-
Some(cls)
243-
} catch {
244-
case e: ClassNotFoundException => None
245-
}
246-
}
243+
def findClass(className: String, classLoader: ClassLoader): Option[Class[_]] =
244+
Try( /*cache.getOrElseUpdate(className, */ classLoader.loadClass(className) /*)*/ ) match {
245+
case Success(cls) => Some(cls)
246+
case Failure(_) => None
247247
}
248-
}
248+
// synchronized {
249+
// cache.get(className).orElse {
250+
// try {
251+
// val cls = classLoader.loadClass(className)
252+
// cache(className) = cls
253+
// Some(cls)
254+
// } catch {
255+
// case e: ClassNotFoundException => None
256+
// }
257+
// }
258+
// }
259+
//}
249260

250261
/**
251262
* Compile scala code. It can be found using the above class loader.
@@ -257,7 +268,7 @@ private class StringCompiler(
257268
// ...and 1/2 this line:
258269
compiler.compileSources(sourceFiles)
259270

260-
if (reporter.hasErrors || reporter.hasWarnings) {
271+
if (reporter.hasErrors /* || reporter.hasWarnings*/ ) {
261272
val msgs: List[List[String]] = reporter match {
262273
case collector: MessageCollector =>
263274
collector.messages.toList
@@ -277,12 +288,19 @@ private class StringCompiler(
277288
resetState: Boolean = true,
278289
classLoader: ClassLoader): Class[_] = {
279290
synchronized {
280-
if (resetState) reset()
281-
282291
apply(code)
283-
findClass(className, classLoader).get // fixme
292+
val clazz = findClass(className, classLoader).get // fixme
293+
294+
if (resetState) reset()
295+
clazz
284296
}
285297
}
298+
299+
override def close(): Unit = {
300+
global.cleanup
301+
global.close()
302+
reporter.reset()
303+
}
286304
}
287305

288306
/**
@@ -351,37 +369,32 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) {
351369
}
352370

353371
def execute[T](className: String, code: String, resetState: Boolean, jars: Seq[File]): T = {
354-
val jarUrls = jars
355-
.map(jar => new java.net.URL(s"file://${jar.getAbsolutePath}"))
356-
.toArray
357-
val urlClassLoader =
358-
new URLClassLoader(jarUrls, compiler.getClass.getClassLoader)
359-
val classLoader =
360-
new AbstractFileClassLoader(compilerOutputDir, urlClassLoader)
372+
val jarUrls = jars.map(jar => new java.net.URL(s"file://${jar.getAbsolutePath}"))
373+
val urlClassLoader = new URLClassLoader(jarUrls, compiler.getClass.getClassLoader)
374+
val classLoader = new AbstractFileClassLoader(compilerOutputDir, urlClassLoader)
361375

362376
val cls = compiler(
363377
wrapCodeInClass(className, code),
364378
className,
365379
resetState,
366380
classLoader
367381
)
368-
cls
382+
383+
val res = cls
369384
.getConstructor()
370385
.newInstance()
371386
.asInstanceOf[() => T]
372387
.apply()
373388
.asInstanceOf[T]
389+
390+
urlClassLoader.close()
391+
392+
res
374393
}
375394

376-
/**
377-
* Check if code is Eval-able.
378-
* @throws CompilerException if not Eval-able.
379-
*/
380-
def check(code: String) = {
381-
val id = uniqueId(code)
382-
val className = "Evaluator__" + id
383-
val wrappedCode = wrapCodeInClass(className, code)
384-
compiler(wrappedCode)
395+
def clean(): Unit = {
396+
compiler.close()
397+
compilerMessageHandler.foreach(_.reset())
385398
}
386399

387400
private[this] def uniqueId(code: String, idOpt: Option[Int] = Some(Eval.jvmId)): String = {
@@ -459,9 +472,16 @@ class ${className} extends (() => Any) with java.io.Serializable {
459472
val jarFile = currentClassPath(0)
460473
val relativeRoot =
461474
new File(jarFile).getParentFile()
462-
val nestedClassPath =
463-
new JarFile(jarFile).getManifest.getMainAttributes
464-
.getValue("Class-Path")
475+
val nestedClassPath = Try {
476+
val jar = new JarFile(jarFile)
477+
val CP = jar.getManifest.getMainAttributes.getValue("Class-Path")
478+
jar.close()
479+
CP
480+
} match {
481+
case Success(classPath) => classPath
482+
case Failure(throwable) =>
483+
throw new CompilerException(List(List(throwable.getMessage)))
484+
}
465485
if (nestedClassPath eq null) {
466486
Nil
467487
} else {
@@ -496,4 +516,5 @@ object Eval {
496516

497517
class CompilerException(val messages: List[List[String]])
498518
extends Exception("Compiler exception " + messages.map(_.mkString("\n")).mkString("\n"))
519+
499520
}

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -115,16 +115,16 @@ object EvaluatorServer extends IOApp {
115115
lazy val port = (Option(System.getenv("PORT")) orElse
116116
Option(System.getProperty("http.port"))).map(_.toInt).getOrElse(8080)
117117

118-
val evaluator = new Evaluator[IO](10.seconds)
118+
val httpApp = auth[IO](service(new Evaluator[IO](10.seconds)))
119119

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

123123
BlazeServerBuilder[IO]
124124
.bindHttp(port, ip)
125-
.withHttpApp(auth[IO](service(evaluator)))
126-
.serve
127-
.compile
128-
.lastOrError
125+
.withHttpApp(httpApp)
126+
.resource
127+
.use(_ => IO.never)
128+
.as(ExitCode.Success)
129129
}
130130
}

server/src/test/scala/org/scalaexercises/evaluator/EvaluatorSpec.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import scala.language.postfixOps
1717

1818
class EvaluatorSpec extends AnyFunSpec with Matchers with Implicits {
1919

20-
def evaluator = new Evaluator[IO](10 seconds)
20+
val evaluator = new Evaluator[IO](10 seconds)
2121

2222
describe("evaluation") {
2323
it("can evaluate simple expressions, for Scala 2.11") {

server/src/test/scala/org/scalaexercises/evaluator/helper.scala

+1-4
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,6 @@ Asserts.scalaTestAsserts($assertCheck)
6060

6161
val fetchCode =
6262
"""
63-
import java.util.concurrent.ScheduledThreadPoolExecutor
64-
6563
import scala.concurrent.ExecutionContext
6664
6765
import cats.data.NonEmptyList
@@ -70,8 +68,7 @@ Asserts.scalaTestAsserts($assertCheck)
7068
import fetch._
7169
import cats.implicits._
7270
73-
val executor = new ScheduledThreadPoolExecutor(4)
74-
val executionContext: ExecutionContext = ExecutionContext.fromExecutor(executor)
71+
val executionContext: ExecutionContext = ExecutionContext.fromExecutor(new java.util.concurrent.ForkJoinPool(2))
7572
7673
implicit val timer: Timer[IO] = IO.timer(executionContext)
7774
implicit val cs: ContextShift[IO] = IO.contextShift(executionContext)

0 commit comments

Comments
 (0)