Skip to content

Commit f0aa010

Browse files
authored
Fix race condition of timeout thread interrupt to stabilize multi level build tests (#4254)
The basic problem was that `thread.interrupt()` running before `interrupt = false` meant there was a chance the thread's `if (interrupt) {` conditional would be `true`, resulting in the timeout thread timing out the open socket immediately, and the Mill server process shutting down. The solution is to move `interrupt = false` to before `thread.interrupt()` This should hopefully fix the flakiness in multi-level-build tests, where the process shutting down would cause the next command to re-spawn a new process, resulting in all classloaders to be re-spawned, violating our assertions Improved the test error checking so next time a similar thing happens, we get more precise reporting "an unwanted process restart occurred", rather than just "classloader invalidation was unexpected". Tested manually with `while ./mill 'integration.invalidation[multi-level-editing].server.test'; do :; done`. This seems to reproduce the problem on my laptop in a few tens of minutes without this fix, after this fix I haven't managed to make it appear
1 parent e3ee143 commit f0aa010

File tree

2 files changed

+24
-2
lines changed

2 files changed

+24
-2
lines changed

integration/invalidation/multi-level-editing/src/MultiLevelBuildTests.scala

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import scala.util.matching.Regex
1515
// in all cases.
1616
trait MultiLevelBuildTests extends UtestIntegrationTestSuite {
1717
var savedClassLoaderIds: Seq[Option[Int]] = Nil
18+
var savedServerId: String = ""
1819
val retryCount: Int = if (sys.env.contains("CI")) 2 else 0
1920
def runAssertSuccess(tester: IntegrationTester, expected: String): Unit = {
2021
val res = tester.eval("foo.run")
@@ -104,6 +105,19 @@ trait MultiLevelBuildTests extends UtestIntegrationTestSuite {
104105
tester: IntegrationTester,
105106
expectedChanged0: java.lang.Boolean*
106107
): Unit = {
108+
109+
// Before checking classloaders, make sure we check to ensure server spawns and
110+
// restarts behave as expected:
111+
if (clientServerMode) {
112+
// Only one server should be running at any point in time
113+
val Seq(serverFolder) = os.list(tester.workspacePath / "out" / "mill-server")
114+
115+
// client-server mode should never restart in these tests and preserve the same process,
116+
val currentServerId = os.read(serverFolder / "serverId")
117+
assert(currentServerId == savedServerId || savedServerId == "")
118+
savedServerId = currentServerId
119+
}
120+
107121
val currentClassLoaderIds =
108122
for ((frame, path) <- loadFrames(tester, expectedChanged0.length))
109123
yield frame.classLoaderIdentity
@@ -132,7 +146,7 @@ trait MultiLevelBuildTests extends UtestIntegrationTestSuite {
132146
object MultiLevelBuildTestsValidEdits extends MultiLevelBuildTests {
133147
val tests: Tests = Tests {
134148
savedClassLoaderIds = Seq.empty[Option[Int]]
135-
149+
savedServerId = ""
136150
test("validEdits") - retry(retryCount) {
137151
integrationTest { tester =>
138152
import tester._
@@ -255,6 +269,7 @@ object MultiLevelBuildTestsValidEdits extends MultiLevelBuildTests {
255269
object MultiLevelBuildTestsParseErrorEdits extends MultiLevelBuildTests {
256270
val tests: Tests = Tests {
257271
savedClassLoaderIds = Seq.empty[Option[Int]]
272+
savedServerId = ""
258273
test("parseErrorEdits") - retry(retryCount) {
259274
integrationTest { tester =>
260275
import tester._
@@ -328,6 +343,7 @@ object MultiLevelBuildTestsParseErrorEdits extends MultiLevelBuildTests {
328343
object MultiLevelBuildTestsCompileErrorEdits extends MultiLevelBuildTests {
329344
val tests: Tests = Tests {
330345
savedClassLoaderIds = Seq.empty[Option[Int]]
346+
savedServerId = ""
331347
test("compileErrorEdits") - retry(retryCount) {
332348
integrationTest { tester =>
333349
import tester._
@@ -416,6 +432,7 @@ object MultiLevelBuildTestsCompileErrorEdits extends MultiLevelBuildTests {
416432
object MultiLevelBuildTestsRuntimeErrorEdits extends MultiLevelBuildTests {
417433
val tests: Tests = Tests {
418434
savedClassLoaderIds = Seq.empty[Option[Int]]
435+
savedServerId = ""
419436
test("runtimeErrorEdits") - retry(retryCount) {
420437
integrationTest { tester =>
421438
import tester._

main/server/src/mill/main/server/Server.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ abstract class Server[T](
6565
) ()
6666
serverLog("server loop ended")
6767
}.getOrElse(throw new Exception("Mill server process already present"))
68+
} catch {
69+
case e: Throwable =>
70+
serverLog("server loop error: " + e)
71+
serverLog("server loop stack trace: " + e.getStackTrace.mkString("\n"))
72+
throw e
6873
} finally {
6974
serverLog("finally exitServer")
7075
exitServer()
@@ -139,8 +144,8 @@ abstract class Server[T](
139144
else res
140145

141146
} finally {
142-
thread.interrupt()
143147
interrupt = false
148+
thread.interrupt()
144149
}
145150
}
146151

0 commit comments

Comments
 (0)