@@ -4,16 +4,22 @@ package workers.common
4
4
import xsbti .compile .ScalaInstance
5
5
import java .io .File
6
6
import java .net .URLClassLoader
7
- import java .nio .file .{FileAlreadyExistsException , Files , Path , Paths }
7
+ import java .nio .file .{FileAlreadyExistsException , Files , Path , Paths , StandardCopyOption }
8
8
import java .util .Properties
9
9
import java .util .concurrent .ConcurrentHashMap
10
10
import scala .collection .immutable .TreeMap
11
+ import scala .util .control .NonFatal
11
12
12
13
object AnnexScalaInstance {
13
14
// See the comment on getAnnexScalaInstance as to why this is necessary
14
15
private val instanceCache : ConcurrentHashMap [Set [Path ], AnnexScalaInstance ] =
15
16
new ConcurrentHashMap [Set [Path ], AnnexScalaInstance ]()
16
17
18
+ // The worker will use this directory to store temp files in order to better perform
19
+ // atomic file copies.
20
+ private val tmpWorkerJarDir = Paths .get(" annex-tmp-worker-jars" )
21
+ Files .createDirectories(tmpWorkerJarDir)
22
+
17
23
/**
18
24
* We only need to care about minimizing the number of AnnexScalaInstances we create if things are being run as a
19
25
* worker. Otherwise just create the AnnexScalaInstance and be done with it because the process won't be long lived.
@@ -106,18 +112,30 @@ object AnnexScalaInstance {
106
112
// This should only happen once per compiler version, so it shouldn't happen often.
107
113
workRequestJarToWorkerJar.foreach { case (workRequestJar, workerJar) =>
108
114
this .synchronized {
109
- // Check for existence of the file just in case another request is also writing these jars
110
- // Copying a file is not atomic, so we don't want to end up in a funky state where two
111
- // copies of the same file happen at the same time and cause something bad to happen.
112
- if (! Files .exists(workerJar)) {
115
+ // Do a more atomic copy of a file by creating a temp file and then moving
116
+ // the temp file to the destination. We can do a move atomically, but cannot do
117
+ // a copy atomically. Copying risks the file existing at the destination in a
118
+ // partially completed state.
119
+ if (Files .notExists(workerJar)) {
120
+ var tmpWorkerJar : Option [Path ] = None
113
121
try {
122
+ tmpWorkerJar = Some (Files .createTempFile(tmpWorkerJarDir, workerJar.getFileName.toString, " tmp" ))
123
+ Files .copy(workRequestJar, tmpWorkerJar.get, StandardCopyOption .REPLACE_EXISTING )
124
+
114
125
Files .createDirectories(workerJar.getParent())
115
- Files .copy(workRequestJar , workerJar)
126
+ Files .move(tmpWorkerJar.get , workerJar, StandardCopyOption . ATOMIC_MOVE )
116
127
} catch {
117
- // We do not care if the file already exists
118
- case _ : FileAlreadyExistsException => {}
119
- case e : Throwable => throw new Exception (" Error adding file to instance cache" , e)
128
+ case NonFatal (e) =>
129
+ throw new Exception (s " Error copying worker jar: ${workerJar}" , e)
130
+ } finally {
131
+ tmpWorkerJar.foreach { tmpWorkerJar =>
132
+ Files .deleteIfExists(tmpWorkerJar)
133
+ }
120
134
}
135
+ } else if (! Files .exists(workerJar)) {
136
+ // Files.exists is not the complement of Files.notExists because both return false
137
+ // when the existence of the file cannot be determined.
138
+ throw new Exception (s " Cannot determine existence of worker jar: ${workerJar}" )
121
139
}
122
140
}
123
141
}
0 commit comments