Skip to content

Commit c4f34cb

Browse files
committed
Merge pull request #25 from RichardBradley/issue-19-windows-race-condition-file-per-thread
#19 Fix the Windows race condition in the measurement file by using one file per thread.
2 parents e31a669 + af0c4b0 commit c4f34cb

File tree

4 files changed

+77
-11
lines changed

4 files changed

+77
-11
lines changed

src/main/scala/scoverage/IOUtils.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ object IOUtils {
77

88
// loads all the invoked statement ids
99
def invoked(dir: File): Seq[Int] = {
10-
dir.listFiles.map(file => {
10+
dir.listFiles.flatMap { file =>
1111
val reader = new BufferedReader(new FileReader(file))
1212
val line = reader.readLine()
1313
reader.close()
1414
line.split(";").filterNot(_.isEmpty).map(_.toInt)
15-
}).flatten.toSeq
15+
}
1616
}
1717

1818
def serialize(coverage: Coverage, file: File) {

src/main/scala/scoverage/Invoker.scala

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ object Invoker {
88
/**
99
* We record that the given id has been invoked by appending its id to the coverage
1010
* data file.
11+
*
1112
* This will happen concurrently on as many threads as the application is using,
12-
* but appending small amounts of data to a file is atomic on both POSIX and Windows
13-
* if it is a single write of a small enough string.
13+
* so we use one file per thread, named for the thread id.
1414
*
15-
* @see http://stackoverflow.com/questions/1154446/is-file-append-atomic-in-unix
16-
* @see http://stackoverflow.com/questions/3032482/is-appending-to-a-file-atomic-with-windows-ntfs
15+
* This method is not thread-safe if the threads are in different JVMs, because
16+
* the thread IDs may collide.
17+
* You may not use `scoverage` on multiple processes in parallel without risking
18+
* corruption of the measurement file.
1719
*/
1820
def invoked(id: Int, path: String) = {
19-
val dir = new File(path)
20-
dir.mkdirs()
21-
val file = new File(path + "/" + Thread.currentThread.getId)
21+
// Each thread writes to a separate measurement file, to reduce contention
22+
// and because file appends via FileWriter are not atomic on Windows.
23+
val file = new File(path, Thread.currentThread.getId.toString)
2224
val writer = new FileWriter(file, true)
2325
writer.append(id.toString + ';')
2426
writer.close()

src/main/scala/scoverage/plugin.scala

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ class ScoverageComponent(val global: Global, options: ScoverageOptions)
7676
def safeSource(tree: Tree): Option[SourceFile] = if (tree.pos.isDefined) Some(tree.pos.source) else None
7777

7878
def invokeCall(id: Int): Tree = {
79-
val file = Env.measurementFile(options.dataDir).getAbsolutePath
8079
Apply(
8180
Select(
8281
Select(
@@ -90,7 +89,7 @@ class ScoverageComponent(val global: Global, options: ScoverageOptions)
9089
Constant(id)
9190
),
9291
Literal(
93-
Constant(file)
92+
Constant(getMeasurementFilesPath())
9493
)
9594
)
9695
)
@@ -140,6 +139,20 @@ class ScoverageComponent(val global: Global, options: ScoverageOptions)
140139
}
141140
}
142141

142+
/**
143+
* Get the configured dir in which the measurement files should be written
144+
* (see [[Invoker.invoked()]])
145+
*
146+
* This path will be hardcoded into the instrumented class files.
147+
*
148+
* We create the dir now (rather than at client runtime), for efficiency.
149+
*/
150+
def getMeasurementFilesPath(): String = {
151+
val dir = Env.measurementFile(options.dataDir).getAbsoluteFile
152+
dir.mkdirs()
153+
dir.getPath
154+
}
155+
143156
def isIncluded(t: Tree): Boolean = {
144157
new CoverageFilter(options.excludedPackages).isIncluded(t.symbol.fullNameString)
145158
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package scoverage
2+
3+
import org.scalatest.FunSuite
4+
import org.scalatest.BeforeAndAfter
5+
import java.io.File
6+
import scala.concurrent._
7+
import scala.concurrent.duration._
8+
import ExecutionContext.Implicits.global
9+
import scala.collection.breakOut
10+
11+
/**
12+
* Verify that [[Invoker.invoked()]] is thread-safe
13+
*/
14+
class InvokerConcurrencyTest extends FunSuite with BeforeAndAfter {
15+
16+
val measurementFile = new File("invoker-test.measurement")
17+
18+
before {
19+
deleteMeasurementFiles()
20+
}
21+
22+
test("calling Invoker.invoked on multiple threads does not corrupt the measurement file") {
23+
24+
val testIds: Set[Int] = (1 to 1000).toSet
25+
26+
// Create 1k "invoked" calls on the common thread pool, to stress test
27+
// the method
28+
val futures: List[Future[Unit]] = testIds.map { i: Int =>
29+
future {
30+
Invoker.invoked(i, measurementFile.toString)
31+
}
32+
}(breakOut)
33+
34+
futures.foreach(Await.result(_, 1.second))
35+
36+
// Now verify that the measurement file is not corrupted by loading it
37+
val idsFromFile = IOUtils.invoked(measurementFile).toSet
38+
39+
idsFromFile === testIds
40+
}
41+
42+
after {
43+
deleteMeasurementFiles()
44+
}
45+
46+
private def deleteMeasurementFiles(): Unit = {
47+
measurementFile.getAbsoluteFile.getParentFile.listFiles()
48+
.filter(_.getName.startsWith(measurementFile.getName))
49+
.foreach(_.delete())
50+
}
51+
}

0 commit comments

Comments
 (0)