@@ -2,23 +2,41 @@ package higherkindness.rules_scala
2
2
package workers .common
3
3
4
4
import xsbti .compile .ScalaInstance
5
- import java .io .File
5
+ import java .io .{ File , IOException }
6
6
import java .net .URLClassLoader
7
- import java .nio .file .{FileAlreadyExistsException , Files , Path , Paths }
7
+ import java .nio .file .{AtomicMoveNotSupportedException , 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
+ /**
19
+ * The worker will use this directory to store temp files in order to better perform atomic file copies.
20
+ */
21
+ private val tmpWorkerJarDir = Paths .get(" annex-tmp-worker-jars" )
22
+ Files .createDirectories(tmpWorkerJarDir)
23
+
24
+ /**
25
+ * The worker will store compiler classpath jars in this directory to enable sharing of classloaders used by the Scala
26
+ * compiler across compilation requests.
27
+ */
28
+ private val workerJarDir = Paths .get(" work-request-jars" )
29
+ Files .createDirectories(workerJarDir)
30
+
17
31
/**
18
32
* We only need to care about minimizing the number of AnnexScalaInstances we create if things are being run as a
19
33
* worker. Otherwise just create the AnnexScalaInstance and be done with it because the process won't be long lived.
20
34
*/
21
- def getAnnexScalaInstance (allJars : Array [File ], workDir : Path , isWorker : Boolean ): AnnexScalaInstance = {
35
+ def getAnnexScalaInstance (
36
+ allJars : Array [File ],
37
+ workDir : Path ,
38
+ isWorker : Boolean ,
39
+ ): AnnexScalaInstance = {
22
40
if (isWorker) {
23
41
getAnnexScalaInstance(allJars, workDir)
24
42
} else {
@@ -81,7 +99,7 @@ object AnnexScalaInstance {
81
99
absoluteWorkDir.relativize(absoluteJarPath),
82
100
replaceExternal = false ,
83
101
)
84
- mapBuilder.addOne(jar.toPath -> comparablePath)
102
+ mapBuilder.addOne(jar.toPath -> workerJarDir.resolve( comparablePath) )
85
103
keyBuilder.addOne(comparablePath)
86
104
}
87
105
val workRequestJarToWorkerJar = mapBuilder.result()
@@ -101,40 +119,80 @@ object AnnexScalaInstance {
101
119
val key = keyBuilder.result()
102
120
103
121
Option (instanceCache.get(key)).getOrElse {
104
- // Copy all the jars to the worker's directory because in a sandboxed world the
105
- // jars can go away after the work request, so we can't rely on them sticking around.
106
- // This should only happen once per compiler version, so it shouldn't happen often.
107
- workRequestJarToWorkerJar.foreach { case (workRequestJar, workerJar) =>
108
- 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)) {
113
- try {
114
- Files .createDirectories(workerJar.getParent())
115
- Files .copy(workRequestJar, workerJar)
116
- } 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)
122
+ this .synchronized {
123
+ // Requests that need the same Scala instance will likely race to this point to create
124
+ // the same Scala instance. This is especially true as the worker is first starting up.
125
+ // Considering that, we first check if the desired instance now exists to avoid duplicate work.
126
+ Option (instanceCache.get(key)).getOrElse {
127
+ // Copy all the jars to the worker's directory because in a sandboxed world the
128
+ // jars can go away after the work request, so we can't rely on them sticking around.
129
+ // This should only happen once per compiler version, so it shouldn't happen often.
130
+ workRequestJarToWorkerJar.foreach { case (workRequestJar, workerJar) =>
131
+ // Do a more atomic copy of a file by creating a temp file and then moving
132
+ // the temp file to the destination. We can do a move atomically, but cannot do
133
+ // a copy atomically. Copying directly to the destination file risks the file existing
134
+ // at the destination in a partially completed state.
135
+ if (Files .notExists(workerJar)) {
136
+ var tmpWorkerJar : Option [Path ] = None
137
+
138
+ try {
139
+ tmpWorkerJar = Some (Files .createTempFile(tmpWorkerJarDir, workerJar.getFileName.toString, " tmp" ))
140
+
141
+ Files .copy(
142
+ workRequestJar,
143
+ tmpWorkerJar.get,
144
+ StandardCopyOption .REPLACE_EXISTING ,
145
+ StandardCopyOption .COPY_ATTRIBUTES ,
146
+ )
147
+ Files .createDirectories(workerJar.getParent())
148
+
149
+ try {
150
+ Files .move(tmpWorkerJar.get, workerJar, StandardCopyOption .ATOMIC_MOVE )
151
+ } catch {
152
+ case e : AtomicMoveNotSupportedException =>
153
+ // Fall back to regular move when ATOMIC_MOVE isn't supported.
154
+ // Because it's not atomic, there's a risk the file may already exist.
155
+ try {
156
+ Files .move(tmpWorkerJar.get, workerJar)
157
+ } catch {
158
+ case e : FileAlreadyExistsException => {}
159
+ }
160
+ }
161
+ } catch {
162
+ case e @ (_ : IOException | _ : InterruptedException ) =>
163
+ // An error occurred which may have left a partially written file, so we delete the
164
+ // file to be safe.
165
+ // Note that this could be a ClosedByInterruptException, which is a subtype of
166
+ // IOException and indicates the operation was interrupted (very likely because
167
+ // the Bazel request was cancelled).
168
+ Files .deleteIfExists(workerJar)
169
+ throw e
170
+ case NonFatal (e) =>
171
+ throw new Exception (s " Error copying worker jar: ${workerJar}" , e)
172
+ } finally {
173
+ tmpWorkerJar.foreach { tmpWorkerJar =>
174
+ Files .deleteIfExists(tmpWorkerJar)
175
+ }
176
+ }
177
+ } else if (! Files .exists(workerJar)) {
178
+ // Files.exists is not the complement of Files.notExists because both return false
179
+ // when the existence of the file cannot be determined.
180
+ throw new Exception (s " Cannot determine existence of worker jar: ${workerJar}" )
120
181
}
121
182
}
122
- }
123
- }
124
183
125
- val instance = new AnnexScalaInstance (Array .from(workRequestJarToWorkerJar.values.map(_.toFile())))
126
- val instanceInsertedByOtherThreadOrNull = instanceCache.putIfAbsent(key, instance)
184
+ val instance = new AnnexScalaInstance (Array .from(workRequestJarToWorkerJar.values.map(_.toFile())))
185
+ val instanceInsertedByOtherThreadOrNull = instanceCache.putIfAbsent(key, instance)
127
186
128
- // putIfAbsent is atomic, but there exists time between the get and the putIfAbsent.
129
- // This handles the scenario in which the AnnexScalaInstance is created and inserted
130
- // by another thread after we ran our .get.
131
- // We could also handle this by generating the AnnexScalaInstance every time and only
132
- // using a putIfAbsent, but that's likely more expensive because of all the classloaders
133
- // that get constructed when creating an AnnexScalaInstance.
134
- if (instanceInsertedByOtherThreadOrNull == null ) {
135
- instance
136
- } else {
137
- instanceInsertedByOtherThreadOrNull
187
+ // putIfAbsent is atomic, but there could exist a time between the get and the putIfAbsent
188
+ // in which the AnnexScalaInstance is created and inserted by another thread. Depends on
189
+ // how things are synchronized.
190
+ if (instanceInsertedByOtherThreadOrNull == null ) {
191
+ instance
192
+ } else {
193
+ instanceInsertedByOtherThreadOrNull
194
+ }
195
+ }
138
196
}
139
197
}
140
198
}
0 commit comments